AsyncTask的原理解析及使用缺陷

AsyncTask是Android從1.5引入的類,它內部做了比較完整的封裝。藉助它,我們可以很easy的在子線程進行耗時操作,在主線程處理結果,而不用操作Thread或者Handler。即使到目前,用AsyncTask做類似文件的上傳或下載也是很方便的。

使用就不多介紹了,就是按照AsyncTask的泛型定義,自己構造子類繼承AsyncTask。

初始化

繼承AsyncTask的子類在初始化的時候,會調用AsyncTask的無參構造函數,構造函數必須在UI線程調用,具體原因後說。查看源碼,精簡後如下:

mHandler=getMainHandler();
mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
               ......
            }
        };

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

這段代碼,初始化了一個Handler對象mHandler,一個Callable對象mWorker和一個FutureTask對象mFuture,並且將mWorker作爲mFuture的參數傳入。但這些只是定義好了回調和一些異常處理,還沒有真正的觸發執行。

任務的執行

開啓任務,需要執行execute(),具體源碼:

@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params){
    return executeOnExecutor(sDefaultExecutor, params);
}

註釋寫的很清楚了,必須在主線程調用。接着看:

@MainThread
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,Params... params) {
    if (mStatus != Status.PENDING) {
        switch (mStatus) {
            case RUNNING:
                throw new IllegalStateException("Cannot execute task:"
                        + " the task is already running.");
            case FINISHED:
                throw new IllegalStateException("Cannot execute task:"
                        + " the task has already been executed "
                        + "(a task can be executed only once)");
        }
    }

    mStatus = Status.RUNNING;

    onPreExecute();

    mWorker.mParams = params;
    exec.execute(mFuture);

    return this;
}

這裏就比較清楚了,首先對mStatus做了狀態判斷,證實任務處於未執行狀態時,將任務的狀態置爲執行中,同時調用onPreExecute,這裏子類可以做一些初始化工作。因爲execute在主線程執行,因此該方法也是在UI線程中被調用。最後用傳進來的Executor對象去執行mFuture中定義的任務,因爲FutureTask也是實現了Runnable接口的。這裏的Executor對象就是sDefaultExecutor,這是個什麼東西?

sDefaultExecutor

private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

在AsyncTask初始化的時候就爲我們提供了一個默認的Executor對象,並且是靜態類型的,也就是說該進程內所有的AsyncTask子類都共用這一個對象。再來看SerialExecutor的源碼:

private static class SerialExecutor implements Executor {
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    Runnable mActive;

    public synchronized void execute(final Runnable r) {
        mTasks.offer(new Runnable() {
            public void run() {
                try {
                    r.run();
                } finally {
                    scheduleNext();
                }
            }
        });
        if (mActive == null) {
            scheduleNext();
        }
    }

    protected synchronized void scheduleNext() {
        if ((mActive = mTasks.poll()) != null) {
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}

該類內部維護了一個雙端隊列mTasks,當開始執行時新建一個Runnable放入mTasks隊尾,這裏面調用mFuture的run方法,點到FutureTask的源碼裏面可以看到,它引用了初始化時傳入的Callable對象,也就是mWorker,並調用其call方法,這就和前面構造函數初始化的時候串起來了。

前面說了,一個進程中不管new多少AsyncTask實例都是公用的一個SerialExecutor。實際上新建一個Runnable加入到隊尾,並沒有觸發任務開始,什麼時候開始執行呢?就是THREAD_POOL_EXECUTOR.execute(mActive)。在第一個任務調用execute方法時,mActive爲null,執行scheduleNext方法。該方法從隊列頭部取值如果不爲空,則賦值給mActive並開始執行。如果該任務耗時比較長,那麼下一個任務進來的時候mActive不爲null,不會立即執行scheduleNext。這時候真正執行的時機是,第一個任務在自己執行完畢後一定要走finally代碼塊中的scheduleNext,這時第二個任務已經被加入到隊尾中,自然就可以保證執行了。這說明SerialExecutor實際上是一個單線程類型的Executor,在任務耗時稍長而有多個任務的情況下,同一時刻只有一個任務執行,執行完畢後才能執行下一個任務。

THREAD_POOL_EXECUTOR

SerialExecutor這個線程池保證了任務的線性執行,真正執行任務的線程池是THREAD_POOL_EXECUTOR。該線程池在AsyncTask類被加載時創建:

private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
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;
private static final int KEEP_ALIVE_SECONDS = 30;

private static final ThreadFactory sThreadFactory = new ThreadFactory() {
    private final AtomicInteger mCount = new AtomicInteger(1);

    public Thread newThread(Runnable r) {
        return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
    }
};

private static final BlockingQueue<Runnable> sPoolWorkQueue =
        new LinkedBlockingQueue<Runnable>(128);
static {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            sPoolWorkQueue, sThreadFactory);
    threadPoolExecutor.allowCoreThreadTimeOut(true);
    THREAD_POOL_EXECUTOR = threadPoolExecutor;
}

AsnycTask默認構造的這個線程池,核心線程和最大線程數和CPU期望核心數有關,keepAliveTime爲30s,阻塞隊列長度爲128。最大線程數雖然不大,但是因爲任務實際是單線程執行的,並不會拋異常。

任務的執行流程

回過頭來,再看異步任務mWorker和mFuture的初始化過程:


mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);
                Result result = null;
                try {
                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                    //noinspection unchecked
                    result = doInBackground(mParams);
                    Binder.flushPendingCommands();
                } catch (Throwable tr) {
                    mCancelled.set(true);
                    throw tr;
                } finally {
                    postResult(result);
                }
                return result;
            }
        };

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

前面說過FutureTask的run()會調用傳入的Callable對象的call(),也就是mWorker的call(),一旦call開始執行,mTaskInvoked置爲true,這是一個原子型變量,標記任務開始執行。try代碼塊中又調用了doInBackground(mParams),這樣就完成了異步調用。過程中如果發生異常,原子變量mCancelled會置爲true。最終獲取doInBackground的返回結果,並傳遞給postResult。該方法源碼:

private Result postResult(Result result) {
    @SuppressWarnings("unchecked")
    Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
            new AsyncTaskResult<Result>(this, result));
    message.sendToTarget();
    return result;
}

顯然這個就是Handler的異步消息處理機制。其中的AsyncTaskResult是AsyncTask內部封裝的一個消息實體。而發送消息的Handler就是getHandler()返回的mHandler,它是在mWorker初始化之前通過getMainHandler()創建了一個靜態內部類InternalHandler對象,並且和主線程綁定:

private static class InternalHandler extends Handler {
    public InternalHandler(Looper looper) {
        super(looper);
    }

    @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
    @Override
    public void handleMessage(Message msg) {
        AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
        switch (msg.what) {
            case MESSAGE_POST_RESULT:
                // There is only one result
                result.mTask.finish(result.mData[0]);
                break;
            case MESSAGE_POST_PROGRESS:
                result.mTask.onProgressUpdate(result.mData);
                break;
        }
    }
}

這裏就可以看到,handleMessage裏就切回到了主線程,並開始處理接收到的異步信息。因爲前面發送的是MESSAGE_POST_RESULT,而result.mTask就是AsyncTask對象本身,所以是調用了類內部的finish()

private void finish(Result result) {
    if (isCancelled()) {
        onCancelled(result);
    } else {
        onPostExecute(result);
    }
    mStatus = Status.FINISHED;
}

到這裏,如果mCancelled不爲true,也就是在上述任務執行過程中未發生異常或主動取消,那麼就會調用onPostExecute(result),所以我們可以重寫這個方法,在UI線程處理返回的結果。當然如果取消了任務,也可以重寫onCancelled(result)做一些異常處理。最後將mStatus置爲FINISHED。

更新任務進度

注意到消息類型還有一個MESSAGE_POST_PROGRESS,發現在publishProgress方法裏發送了這個消息:

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

這個方法AsyncTask本身自己不會調用,而是由使用者在子類的doInBackground中調用。InternalHandler收到消息後開始處理,又會調用onProgressUpdate。這樣使用者可以獲取任務執行的進度,並且在主線程實時更新,最常用的就是下載文件顯示進度條。

使用缺陷

從上面的分析看,AsyncTask爲我們封裝好了一整套Thread、Handler、Executor、Runnable,包括異常處理,泛型和回調,有很好的思想。當然,它也有一些缺陷,無法適用一些場景。

首先就是最初AsyncTask是單線程依次執行,從安卓1.6開始,變爲多線程並行執行,但同時最多支持128個任務併發,一旦任務耗時稍長而又有很多個任務的情況下就很容易拋異常。因此從安卓3.0以後,又引入了SerialExecutor,等於還是默認單線程一個一個任務執行。其實安卓也意識到了這個問題,所以它的executeOnExecutor(Executor exec,Params... params)允許我們可以傳入自己定義的Executor,而不是默認的SerialExecutor。比如下例子:

private void execute() {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 150; i++) {
        new MyTask().executeOnExecutor(executorService);
    }
}

private class MyTask extends AsyncTask<Void, Void, Void> {

    @Override
    protected Void doInBackground(Void... voids) {
        try {
            Log.i(TAG, Thread.currentThread().getName());
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

可以看到快速打印出來的日誌,而不是每隔1s打印一次日誌,確實是並行的。什麼場景要用到並行執行?很典型的一個例子就是本地相冊,因爲Bitmap的解析不能放到UI線程中進行,而如果使用AsyncTask的串行執行,能想像向下滑動時圖片要一個一個的依次加載出來?但是實際上還是有很多坑。

即使是採用並行執行任務,如果相冊裏有幾千張圖片甚至更多,初次加載沒有緩存的話,當快速滑動的時候仍然會發生卡頓。就是當任務負載量短時間內比較高的情況下,doInBackground沒有及時得到執行的原因,在一幀(16ms)內沒有完成任務,造成了丟幀。具體爲什麼,我認爲可能是因爲是使用FutureTask包裝了任務執行的原因,降低了程序整體效率。因爲FutureTask本身就適用於很耗時的任務,而AsyncTask註釋又說明了自身適合的場景是短時間的輕量任務,也不知道谷歌爲何要這麼設計,雖然已經滿足了大部分需求了。除此之外,AsyncTask還存在如在組件中的生命週期、內存泄漏等缺陷。因此高負載量的後臺任務,還是用ThreadPoolExecutor+Runnable比較好。

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