(上篇)解讀AsyncTask

一、前言

爲了提高流暢性,耗時任務放後臺線程運行,這是APP開發的常識了。
遠古時期,還沒有各種庫的時候,用來處理異步任務的方法有:
Thread/ThreadPoolExecutor、Service/IntentService、AsyncTask……

其中,AsyncTask可以說是骨灰級的API了,經過多次迭代。
AsyncTask適用於“數據加載+界面刷新”的模式,而對於Android開發而言,這類模式是比較常見的,所以一度還是很多人使用AsyncTask的。
然而隨着RxJava的普及,AsyncTask日漸式微,如今或許還有存在於一些舊代碼中,或許還有部分開發者還在使用。
AsyncTask最終是否會被掩埋與歷史的塵埃之中,不得而知;
事實上,雖是“古董”,搬出來把玩一番,拭去塵埃,你會發現,破舊的表面之下,也有熠熠生輝之處。

二、原理

我們可以從使用方法切入,看下幾個常用的方法:

  • execute 發起任務;
  • cancel 取消任務
  • onPreExecute 執行任務前回調(UI線程)
  • doInBackground 執行任務,可發佈進度 (後臺線程)
  • onProgreessUpdate 顯示進度(UI線程)
  • onPostExecute 任務結束後回調(UI線程)
  • onCancelled 任務取消後回調(UI線程)

從API文檔我們可以知道:
前兩個需要主動調用,後面是回調方法。
常規流程:execute -> onPreExecute -> doInBackground -> onPostExecute;
doInBackground的過程中可以發佈進度,發佈後會回調 onProgreessUpdate;
如果調用了cancel,執行結束時回調onCancelled。

那麼,這些方法的背後是如何運作的呢? 接下來我們對關鍵代碼稍作分析。
AsyncTask的實現很簡潔,去掉註釋,只有兩三百行代碼;
如果要分析流程,100行左右的核心代碼就夠了。
下面是精簡後的代碼:

public abstract class AsyncTask<Params, Progress, Result> {
    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
    private static InternalHandler sHandler;
    private final WorkerRunnable<Params, Result> mWorker;
    private final FutureTask<Result> mFuture;
    private final AtomicBoolean mTaskInvoked = new AtomicBoolean();

    public AsyncTask() {
        sHandler = new InternalHandler(Looper.getMainLooper());

        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);
                Result result = null;
                try {
                    result = doInBackground(mParams);
                } finally {
                    postResult(result);
                }
                return result;
            }
        };

        mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                postResultIfNotInvoked(get());
            }
        };
    }

    private void postResultIfNotInvoked(Result result) {
        final boolean wasTaskInvoked = mTaskInvoked.get();
        if (!wasTaskInvoked) {
            postResult(result);
        }
    }

    private void postResult(Result result) {
        Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
                new AsyncTaskResult<Result>(this, result));
        message.sendToTarget();
    }

    protected final void publishProgress(Progress... values) {
        if (!isCancelled()) {
            getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                    new AsyncTaskResult<Progress>(this, values)).sendToTarget();
        }
    }

    private static class InternalHandler extends Handler {
        public InternalHandler(Looper looper) {
            super(looper);
        }
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
            if(msg.what == MESSAGE_POST_RESULT){
                result.mTask.finish(result.mData[0]);
            }else if(msg.what == MESSAGE_POST_PROGRESS){
                result.mTask.onProgressUpdate(result.mData);
            }
        }
    }

    private void finish(Result result) {
        if (isCancelled()) {
            onCancelled(result);
        } else {
            onPostExecute(result);
        }
    }

    public final AsyncTask execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }

    public final AsyncTask executeOnExecutor(Executor exec, Params... params) {
        onPreExecute();
        mWorker.mParams = params;
        exec.execute(mFuture);
    }
}

主要執行流程,簡單地說,就是:
Task.execute -> Executor -> FutureTask -> WorkerRunnable -> Task.postResult -> Handler -> Task.finish
除去Task, 就只剩 “Executor + FutureTask + WorkerRunnable + Handler” 了。
所以,如果要分析實現,抓住流程,然後對這幾個部分各個擊破即可。

  • AsyncTask的Executor比較有考究,後面會有相關分析,此處只需知道Executor用於執行Runnable即可。
  • FutureTask實現了Runnable和Future,WorkerRunnable實現了Callable。
    FutureTask傳給Executor後,會分配線程執行FutureTask的run()方法;
    然後通常情況下經歷 FutureTask.run() -> Callable.call() -> FutureTask.done() 的過程,
    如果在 FutureTask.run()之前執行了cancel方法,則call()不會被回調,但done()還是會被回調的。
  • 上面代碼中可以看到,call()方法中執行了doInBackground和postResult;
    done()方法中執行了postResultIfNotInvoked,也就是,如果call()沒有被調用,則執行postResult。
    所以,無論doInBackground有沒有被執行,最終總會執行finish(執行onPostExecuteonCancelled其中之一)。
  • Handler大家都很熟悉了,當傳入MainLooper時,handleMessage在主線程被回調。

下面是AsyncTask的流程圖:

圖片出自《AsyncTask知識掃盲》,筆者做了部分補充。

三、存在的問題

AsyncTask用法很簡單,對Android開發中常見的“數據加載+界面刷新”的場景提供了友好的API。
然而隨着使用的深入,漸漸地大家發現AsyncTask有不少問題:

  • 不好好工作的cancel():有時候起作用;
  • 串行or並行:經歷過串行-並行-串行(默認)的迭代;
  • 內存泄漏:若持有Activity引用,且生命週期比Activity的長,則Activity無法被及時回收;
  • 生命週期:不會隨着Activity的銷燬而銷燬;
    ……還有其他的一些,就不逐一列舉了。

上面這些陳述是不爭的事實,然而對這些事實的定性(是不是問題),仁者見仁,智者見智。

下面是筆者的分析:

3.1 cancel() or not

AsyncTask的cancel確實是不一定能立即取消任務,但這其實是合理的。
當調用AsyncTask的cancel(mayInterruptIfRunning), 並傳入true時,會觸發interrupt()。
interrupt() 雖然不能保證馬上終止任務,但是能夠中斷sleep(), wait()等方法;
比如使用OkHttp時, interrupt()能夠中斷網絡請求,因爲 OkHttp在等待網絡數據時用了wait方法。
爲什麼不用Thread.stop()呢? Thread.stop()是個危險的方法。
比方說一個線程正在寫入數據,如果突然中止,可能會導致數據不爭取,甚至文件格式被破壞。

3.2 串行or並行, that is a question

串行還是並行這個問題,官方文檔有交代:

When first introduced, AsyncTasks were executed serially on a single background thread.
Starting with Build.VERSION_CODES.DONUT , this was changed to a pool of threads allowing multiple tasks to operate in parallel.
Starting with Build.VERSION_CODES.HONEYCOMB , tasks are executed on a single thread to avoid common application errors caused by parallel execution.
If you truly want parallel execution, you can invoke executeOnExecutor(java.util.concurrent.Executor, java.lang.Object[]) with THREAD_POOL_EXECUTOR .

簡單地說,就是:
最初的時候,AsyncTask確實是串行的;
自1.6之後,改成並行了;
自3.0之後,“爲避免並行導致普遍的應用程序錯誤”,又改成串行了;
如果確實想並行,可以調用executeOnExecutor(THREAD_POOL_EXECUTOR)。
(翻譯比較直白,比如parallel譯成並行,雖然按情景似乎併發更合理一點,先按文檔來吧)

API其實提供了串行和並行兩種選擇的,但默認方式只能選一種;
改來改去確實不厚道,但是自從3.0之後就沒有改過了(現在minSdkVersiond基本都4.0以上),所以算是曾經坑過;
最後,默認串行好還是默認並行好?官方人員看來很糾結,但最終選擇了串行。

這就好比建了一個游泳池,深水區和淺水區隔開,由於怕人溺水,默認開放淺水區。
如果你確實想到深水區也可以,就在隔壁。

但淺水區也不是絕對安全的,比如有位開發者就遇到過這樣的“坑”:
他同時用了兩個SDK,一個用來做圖片剪裁,一個是facebook的廣告SDK。
然後發現圖片加載不出來,經過覈查發現兩個SDK都用了AsyncTask, 但都是用的串行的Executor。
國內訪問外網速度偏慢,所以facebook的SDK阻塞了後面的任務(圖片剪裁)。
後來作者給這個圖片剪裁庫的開發者提了建議,讓其改用THREAD_POOL_EXECUTOR來圖片剪裁,方纔解了相互阻塞的問題。

串行和並行其實都是有需求的,需具體問題具體分析。
以現在主流App的複雜度,任務併發幾乎是無法避免的。
總是要經歷大風大浪的,注意(線程)安全就好。

3.3 內存泄漏 & 生命週期

“由於持有Activity引用,且生命週期比Activity長, 導致Activity無法被及時回收。”
看到這段描述,或許很多讀者都能想到Handler了,Handler也有此問題;其實RxJava也有這個問題。
所以這個問題不是AsyncTask獨有。

解決此問題通常有兩種方案:
1、聲明爲靜態內部類,用弱引用持有Activity;
2、解決生命週期問題,隨Activity銷燬而銷燬。

第一種方案操作成本太高,極其不方便;
如果解決生命週期問題,不單內存泄漏問題可解,很多其他的問題也會迎刃而解,可謂一石多鳥。
常見的做法就是在Activity回調函數onDestroy()中調用cancle()方法,
但是這樣的話需要在Activity聲明一個AsyncTask的變量,指向AsyncTask的實例,寫起來也是很麻煩。

3.4 框架定位

前面提到的問題,除了第3點外,其他都是比較模糊的。
在我看來,最大的問題,是AsyncTask的框架定位。
看一段API文檔的描述:

AsyncTask is designed to be a helper class around Thread and Handler and does not constitute a generic threading framework.
AsyncTasks should ideally be used for short operations (a few seconds at the most.)
If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the java.util.concurrent package such as Executor , ThreadPoolExecutor and FutureTask

Google譯文:
AsyncTask旨在成爲Thread和Handler的輔助類,並不構成通用的線程框架。
理想情況下,AsyncTask應該用於短操作(最多幾秒鐘)。
如果需要保持線程長時間運行,強烈建議您使用concurrent包提供的各種API,例如Executor,ThreadPoolExecutor和FutureTask

首先說AsyncTask不是“通用的線程框架”,承認自己是“線程框架”,但又覺得自己不“通用”;
自己其使用了“Executor,ThreadPoolExecutor和FutureTask”這些API,又建議別人去使用這些API。
太過保守了!

裏面還提到,AsyncTask適用於短操作,不適合長時間運行的任務。爲什麼呢?
可以結合代碼(Android 9.0)看一下AsyncTask的線程池配置:

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    // We want at least 2 threads and at most 4 threads in the core pool,
    // preferring to have 1 less than the CPU count to avoid saturating
    // the CPU with background work
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    
    public static final Executor THREAD_POOL_EXECUTOR;
    static {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, 30, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(128), sThreadFactory);
        threadPoolExecutor.allowCoreThreadTimeOut(true);
        THREAD_POOL_EXECUTOR = threadPoolExecutor;
    }

其實CORE_POOL_SIZE大小的設置也是經過很幾個版本的:
先是固定等於5,後來是CPU_COUNT + 1,如今是 max(2, min(CPU_COUNT - 1, 4)
LinkedBlockingQueue的capacity曾經一度是10,然後MAXIMUM_POOL_SIZE是128(這個版本,有人認爲是SDK開發者寫反了)。

要理解這些參數配置,需熟悉Java線程池,這裏推薦閱讀筆者的另一篇文章《速讀Java線程池》

做完廣告,我們來繼續分析代碼-_-
首先是CORE_POOL_SIZE,max(2, min(CPU_COUNT - 1, 4), 非常經典的用法, 最終2<=coreSize<=4。
coreSize太小,幾個任務下來就滿了,如果任務又比較耗時,比如說網絡請求,後面的任務就要等很久了。
這樣的話即使調用executeOnExecutor(THREAD_POOL_EXECUTOR),還是會碰到前面提到的那個“坑”(相互阻塞)。
這就是API文檔說“AsyncTask應該用於短操作”的原因。

那爲什麼coreSize不設置大一點呢?兩個原因:
1、如果是執行的是計算密集型任務,線程數大於CPU核心反而會更慢,因爲線程上下文切換還是有不少消耗的;
2、給UI線程保留計算資源,“to avoid saturating(飽和) the CPU with background work”。

而實際上,開發中需要用到異步的,很多情況下是IO密集型操作,尤其是網絡請求。

“夫雞肋,棄之如可惜,食之無所得。”
AsyncTask可以用來做很多事,但是很多類型的任務又不適合做,這麼一來,就顯得很雞肋了。

四、Left or Right?

古往今來,面臨選擇總是讓人痛苦的。

黃色的樹林裏分出兩條路,
可惜我不能同時去涉足。
—— 弗羅斯特

世間安得雙全法,不負如來不負卿。
—— 倉央嘉措

其實,開發過程中也經常會遇到需要選擇的地方。

面對選擇,內心就像天平一樣,要麼左,要麼右,要麼端平;
如果某一邊有絕對的優勢,這時候沒有疑問會往一邊傾倒;
而比較常見的情況是,隨着需要衡量的參考因素不斷增加而搖擺不定,當然最終還是會有結果。

從前面的分析我看到,AsyncTask在Executor默認串行還是並行方面猶豫不決,最終選擇了串行,
不過還好,同時也提供了executeOnExecutor(THREAD_POOL_EXECUTOR)來支持並行。

coreSize也是經歷過幾個版本,但看得出來SDK的開發者是很糾結的:
模糊地設一個值似乎不太科學,設置大了怕線程池切換和CPU飽和,小了吞吐率又不高……

那是否有“雙全法”,既有效利用CPU,又支持高吞吐率呢?

幸運的是,在這個問題上,有解決方案。
但爲了控制篇幅,我們放在下一篇文章中講解。
預知後事如何,且聽下回分解。

傳送門:(下篇)AsyncTask加強版

參考資料:
https://juejin.im/post/5a85a6066fb9a06337573955
https://cloud.tencent.com/developer/article/1328339
https://www.jianshu.com/p/94a483b4e26c
https://www.zhihu.com/question/41048032
https://www.zhihu.com/question/33515481

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章