深入理解安卓非同步任務AsyncTask

saka發表於2019-02-21

上一節講了asynctask的官方文件,這一節深入講解如何深入使用AsyncTask。

asynctask本質上也是執行緒啟動,只是它封裝了一些內容,可以執行在後臺,同時可以和UI執行緒互動。
asynctask最少要啟動2個執行緒,最多四個。

AsyncTask的狀態

AsyncTask的狀態共有三種,PENDING,RUNNING和FINISHED。這三種狀態在AsyncTask的生命週期中之出現一次。

  1. PENDING,表示非同步任務還沒有開始執行。
  2. RUNNING,表示非同步任務正在執行。
  3. FINISHED,表示onPostExecute已經執行完成。

安卓官方文件推薦我們編寫一個子類繼承自AsyncTask並且必須實現doinbackground方法。
我們編寫一個簡單的程式測試這三個狀態的具體情況。

private ProgressTask task = new ProgressTask();//自定義的一個內部類,繼承自AsyncTask
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_splash);
        ...
        Log.d("task", "before execute task`s status is " + task.getStatus());
        task.execute();
        Log.d("task", "after execute task`s status is " + task.getStatus());複製程式碼

在AsyncTask中我們實現以下幾個方法

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            Log.d("task", "onPreExecute task`s status is " + task.getStatus());
        }
        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            Log.d("task", "onPostExecute task`s status is " + task.getStatus());
            handler.sendEmptyMessage(1);
        }複製程式碼

在handler中我們接收一個資訊,用來列印此時task的狀態

        switch (msg.what) {
            case 1:
                Log.d("task", "finally task`s status " + task.getStatus());
                break;
        }複製程式碼

這樣,我們的程式執行起來看一下日誌

04-02 18:10:43.747 10433-10433/com.example.saka.materialdesignapplication D/task: before execute task`s status is PENDING
04-02 18:10:43.748 10433-10433/com.example.saka.materialdesignapplication D/task: onPreExecute task`s status is RUNNING
04-02 18:10:43.748 10433-10433/com.example.saka.materialdesignapplication D/task: after execute task`s status is RUNNING
04-02 18:11:17.724 10433-10433/com.example.saka.materialdesignapplication D/task: onPostExecute task`s status is RUNNING
04-02 18:11:17.724 10433-10433/com.example.saka.materialdesignapplication D/task: finally task`s status FINISHED複製程式碼

可以看到,在整個任務週期中,task在execute之前是一直處於pennding狀態,
在execute之後onPostExecute中的所有方法執行完成之前一直處於RUNNING狀態,
跳出onPostExecute方法之後所有的task任務相當於已經完成,
這時候task的狀態時FINISHED。
通過觀察原始碼我們可以看一下:
當你新建一個AsyncTask物件以後,系統就會自動生成一個變數:
private volatile Status mStatus = Status.PENDING;
這也就是說明當你new一個AsyncTask後,它的狀態就被設定為了PENDING狀態。
直到execute或者executeOnExecutor方法執行之後。
呼叫task.execute()之後,系統會呼叫executeOnExecutor()方法:

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

傳入的參數列明會啟動預設的執行緒執行器,而現在系統預設的序列執行,也就是分先後次序執行。
executeOnExecutor()方法返回的同樣是一個AsyncTask例項:

    @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;
    }複製程式碼

在這個方法中,首先判斷非同步任務的狀態,
假如這個任務沒有在等待狀態,而是正在執行或者已經結束,會丟擲異常。
然後會將非同步任務的狀態設定為RUNNING,呼叫onPreExecute方法,
並將引數傳遞給AsyncTask中的引數接收器,
然後才開始執行exec.execute(mFuture)方法。所以在onPreExecute之前,狀態已經設定為了RUNNING。
再來研究以下何時會停止。
呼叫exec.execute(mFuture)中傳入的引數是一個介面型別的Runable:

        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)
上面兩步都是在new一個AsyncTask例項的時候執行的,try塊中時執行doInbackGround方法,
在finally中執行的是結束動作。在postResult()方法中,定義了一個message,
呼叫該方法的時候會使用AsyncTask中的handler傳送訊息,handler接收到訊息後執行如下程式碼:

     case MESSAGE_POST_RESULT:
                // There is only one result
                result.mTask.finish(result.mData[0]);
                break;
     ...
     private void finish(Result result) {
             if (isCancelled()) {
                 onCancelled(result);
             } else {
                 onPostExecute(result);
             }
             mStatus = Status.FINISHED;
     }複製程式碼

在finish方法中設定了status為FINISHED,
注意此處時在執行完onCancelled或者onPostExecute之後才改變狀態,
這也就解釋了為什麼在onPostExecute方法中獲取的狀態仍然是RUNNING。

Note:根據文件中的介紹,並沒有講當task被cancel後是否會執行FINISHED,實際是執行的。

取消任務

關於cancel方法官方的介紹比較簡單,參看上篇文章。
首先看一下官方文件對這個API的解釋:
boolean cancel (boolean mayInterruptIfRunning)

嘗試取消執行此任務。 如果任務已經完成,已經被取消或由於某種其他原因而無法取消,則此嘗試將失敗。
如果成功,並且在取消被呼叫時此任務尚未開始,則此任務不應該執行。
如果任務已經啟動,那麼mayInterruptIfRunning引數可以確定執行該任務的執行緒是否應該被中斷,以試圖停止該任務。
呼叫此方法將導致在doInBackground(Object [])返回後在UI執行緒上呼叫onCancelled(Object)。
呼叫此方法可以保證onPostExecute(Object)從不被呼叫。
呼叫此方法後,應該從doInBackground(Object [])定期檢查isCancelled()返回的值,以儘早完成任務。

這是什麼意思呢?

public final boolean cancel(boolean mayInterruptIfRunning) {
        mCancelled.set(true);
        return mFuture.cancel(mayInterruptIfRunning);
    }複製程式碼

呼叫asynctask的cancel方法其實就是呼叫futuretask的cancel方法,這個是java執行緒中的方法,
假如傳入的引數為true的時候(並不推薦這種方式),會立即停止當前任務;
當傳入的引數為false的時候,會等到本次任務執行完成後,
無論在哪種情況下,我們都應該清楚的認識到,
此時獲取當前任務的isCanceled都會返回為true。

呼叫cancel方法影響到的會有doInBackground、onPostExecute和onCancelled方法,
當傳入引數為ture的時候,doInBackGround方法會立即中斷,進入onCancelled方法,而不執行onPostExecute方法;
當傳入的引數為false的時候,doInBackground方法會執行完畢後進入onCancelled方法,也不執行onPostExecute方法;
始終不影響onPreExecute方法,它始終會執行。
但是偶爾會出現doInbackground方法不執行而直接進入onCancelled方法。

  1. task啟動直接cancel:

    private void setCancelNow() {
         task.cancel(false);
     }複製程式碼

    此時日誌是:

    04-02 21:44:24.692 8082-8082/com.example.saka.materialdesignapplication D/task: before execute task`s status is PENDING
    04-02 21:44:24.692 8082-8082/com.example.saka.materialdesignapplication D/task: onPreExecute task`s status is RUNNING
    04-02 21:44:24.692 8082-8082/com.example.saka.materialdesignapplication D/task: after execute task`s status is RUNNING
    04-02 21:44:24.747 8082-8082/com.example.saka.materialdesignapplication D/task: onCancelled task`s status:RUNNING複製程式碼

    可見方法執行了onPreExecute和onCancelled方法,然而並沒有執行doInBackground方法。

  2. task啟動後過一段時間在cancel:

     private void setCancelDelay() {
         try {
             Thread.sleep(1000);
             task.cancel(false);
             Log.d("task", "task after cancel");
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     }複製程式碼

    此時的輸出日誌是:

    04-02 21:51:22.781 14525-14525/? D/task: before execute task`s status is PENDING
    04-02 21:51:22.781 14525-14525/? D/task: onPreExecute task`s status is RUNNING
    04-02 21:51:22.781 14525-14525/? D/task: after execute task`s status is RUNNING
    04-02 21:51:22.782 14525-14540/? D/task: doInBackground
    04-02 21:51:23.113 14525-14540/? D/task: the progress value 0
    04-02 21:51:23.452 14525-14540/? D/task: the progress value 1
    04-02 21:51:23.782 14525-14525/com.example.saka.materialdesignapplication D/task: task after cancel
    ...
    04-02 21:51:56.552 14525-14525/com.example.saka.materialdesignapplication D/task: onCancelled task`s status:RUNNING
    04-02 21:51:56.552 14525-14525/com.example.saka.materialdesignapplication D/task: finally task`s status FINISHED複製程式碼

    可以看到此時執行了doInbackground方法,最後進入了onCancelled方法。

因為preExecute方法是在非同步任務啟動後而真正的執行緒啟動前呼叫的,
而cancel方法呼叫的時futuretask的cancel方法,
也就是真正啟動執行緒之後才執行cancel的,
所以preExecute一定會執行。
doInBackground是在WorkerRunnable的call回撥方法中執行的,

        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);
                ...
                try {
                    ...
                    result = doInBackground(mParams);
                    Binder.flushPendingCommands();
                } catch (Throwable tr) {
                    mCancelled.set(true);
                    throw tr;
                } finally {
                    postResult(result);
                }
                return result;
            }
        };複製程式碼

假如WorkerRunnable不執行,也就不會呼叫onCancelled方法。
futuretask有一個回撥方法是必須實現的,done()方法,當任務結束後會呼叫此方法。
futuretask在done的回撥方法中檢測mTaskInvoked是否被呼叫,假如未被呼叫會執行
postResultIfNotInvoked(get())

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

此時同樣會呼叫postResult進而呼叫onCancelled方法。

所以無論何時都會呼叫onCancelled方法。

任務的序列和並行

首先看看execute方法:
AsyncTask<params, progress,=”” result=””> execute (Params… params)

相關文章