AsyncTask 面試解析

蘆葦科技App技術團隊發表於2019-05-13

[Toc]

基礎認識

AsyncTask 是基於 Handler 進行封裝的輕量級非同步類,它是一個抽象類,我們要使用的時候需要實現其子類的以下 4 個方法

方法 描述
onPreExecute() 任務執行前被呼叫,執行在 UI 執行緒中,在這裡我們做一些任務啟動前的準備
doInBackground() 執行在新的子執行緒中的,做非同步任務的處理
onProgressUpdate() 這個方法是在呼叫 publishProgress 的時候被呼叫的,是執行在 UI 執行緒的
onPostExecute() 這個方法是任務執行完畢之後被呼叫的,是執行在 UI 執行緒中的

作用

  • 實現多執行緒: 在工作執行緒中執行任務,如 耗時任務
  • 非同步通訊、訊息傳遞: 實現工作執行緒 & 主執行緒(UI執行緒)之間的通訊,即:將工作執行緒的執行結果傳遞給主執行緒,從而在主執行緒中執行相關的UI操作

AsyncTask 的三種狀態

每個狀態在一個任務的生命週期中只會被執行一次。

狀態 描述
PENDING 等待(還沒有開始執行任務)
RUNNING 執行中
FINSHED 完成

AsyncTask 的內部執行過程

AsyncTask 的物件呼叫 execute 方法,execute 內部又呼叫了 executeOnExecutor ,onPreExecute 方法就是在這裡被回撥,之後將 AsyncTask 的引數封裝成一個併發類,然後將其新增到排隊執行緒池(SerialExecutor)中進行排隊,如果當前有任務正在執行,則等待,否則 THREAD_POOL_EXECUTOR 執行該任務。在任務的執行過程中,通過 InternalHandler 將進度 pos(MESSAGE_POST_GROGRESS)傳送到主執行緒中,此時會呼叫 onProgressUpdate 方法,任務執行完畢之後,InternalHandler 將結果 post(MESSAGE_POST_RESULT) 傳送到主執行緒中,此時 onPostExecute 或者 onCancle 會被呼叫,任務執行到這裡就結束了。

基本使用

  • 舉個例子:在非同步任務中每隔 1s 列印 1 ~ 10 的數值
    1. 我們不干預任務的執行過程,由任務執行完成,檢視任務執行情況;
    2. 任務執行完成後,我們再點選開始,檢視任務執行情況;
    3. 干預任務的執行過程,在任務執行期間點選取消,檢視任務執行情況;
public class MainActivity extends AppCompatActivity {
    private TextView mText;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mText = findViewById(R.id.text);
        progressAsycn1 = new ProgressAsycn();
        findViewById(R.id.btn_start).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                progressAsycn1.execute(1);
            }
        });
         findViewById(R.id.btn_cancel).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                progressAsycn1.cancel(true);
                mText.append(String.format("取消任務%s\n",new Date().toString()));
            }
        });

    }


    private ProgressAsycn progressAsycn1;
    private class ProgressAsycn extends AsyncTask<Integer,Integer,String> {

        // 這個方法是在啟動之前被呼叫的,執行在 UI 執行緒中,在這裡我們做一些任務啟動前的準備
        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            mText.append(String.format("準備執行%s\n",new Date().toString()));
        }

        // 這個方法是執行在新的執行緒的中的
        @Override
        protected String doInBackground(Integer... params) {
            for (int i = params[0]; i <= 10; i++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                publishProgress(i);
            }
            return "任務已經執行完畢";
        }

        // 這個方法是在呼叫 publishProgress 的時候被呼叫的,是執行在 UI 執行緒的
        @Override
        protected void onProgressUpdate(Integer... values) {
            mText.append(String.format("工作進度:%d\n",values[0]));
        }

        // 這個方法是任務執行完畢之後被呼叫的
        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            mText.append(String.format("任務執行完畢%s\n",new Date().toString()));
        }

        /**
         * 非同步任務被取消時回撥,即 AsyncTask 的物件呼叫了 cancel 方法
         * 這個方法和 onPostExecute 互斥
         * doInBackground 方法中的任務執行完畢,才會被回撥
         */
        @Override
        protected void onCancelled() {
            mText.append(String.format("非同步任務已取消%s\n",new Date().toString()));
        }
    }
}
複製程式碼
  • 執行結果

    1. 不干預任務的執行過程,最後的執行結果如下

    asyncTask1.png
    2. 在 1 執行完之後,再點選開始,結果程式報如下錯誤

        java.lang.IllegalStateException: Cannot execute task: the task has already been executed (a task can be executed only once)
    複製程式碼
    1. 在任務的執行過程中,點選取消任務,呼叫 cancel() 方法,之後我們可以看到 onProgressUpdate() 和 onPostExecute() 方法不再被呼叫,但是我們取消任務的時候,任務還是沒有停止的,等到任務真正停止的時候,onCancelled() 方法被呼叫,執行效果圖如下:

    asyncTask2.png

  • 問題:從上面的執行結果中,我們可以看出兩個問題

      1. AsyncTask 的物件只能被呼叫一次,再次呼叫的時候,會出錯
      1. Asynctask 呼叫了 cancel() 方法取消任務,但是任務並沒有真正的停止

提出問題

我們在閱讀原始碼之前,先給自己提一些問題,然後我們在閱讀原始碼的時候,帶著問題來去找答案,這樣我們的目標才會更加明確。

  1. 為什麼AsyncTask 的物件只能被呼叫一次,否則會出錯?(每個狀態只能執行一次)
  2. AsyncTask 的類為什麼必須在主執行緒載入
  3. AsyncTask 的物件為什麼必須在主執行緒中建立
  4. AsyncTask 是序列執行任務還是並行執行任務?
  5. AsyncTask 呼叫 cancel() 任務是否立即停止執行?onPostExecute() 還會被呼叫嗎?onCancelled() 什麼時候被呼叫?

內部原始碼分析

建構函式中做了什麼

首先,我們檢視在 AsyncTask 的建構函式裡面到底做了些什麼

    /**
     * 該構造方法必須在 UI 執行緒中被呼叫
     */
    public AsyncTask() {
        this((Looper) null);
    }
    
    
    /**
     * 該構造方法必須在 UI 執行緒中被呼叫
     *
     * @hide
     */
    public AsyncTask(@Nullable Looper callbackLooper) {
        mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
            ? getMainHandler()
            : new Handler(callbackLooper);

        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);
                Result result = null;
                try {
                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                    // doInBackground 的呼叫時機
                    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 (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                } catch (ExecutionException e) {
                    throw new RuntimeException("An error occurred while executing doInBackground()",
                            e.getCause());
                } catch (CancellationException e) {
                    postResultIfNotInvoked(null);
                }
            }
        };
    }
複製程式碼

從以上的程式碼中,我們可以看出 AsyncTask 做了以下幾個工作:

  • 初始化 handler

    • 我們可以從預設呼叫的建構函式中傳入的 callbackLooper 是為 null,那麼 handler 初始化的時候是呼叫 getMainHandler() 方法的,getMainHandler() 方法的的具體程式碼如下:
    private static InternalHandler sHandler;
    private static Handler getMainHandler() {
        synchronized (AsyncTask.class) {
            if (sHandler == null) {
                sHandler = new InternalHandler(Looper.getMainLooper());
            }
            return sHandler;
        }
    }    
    複製程式碼
    • 我們從上面的程式碼中可以知道,getMainHandler() 最後返回的值是 sHandler,而 sHandler 是個靜態 InternalHandler 例項,而關於 InternalHandler 的實現,我們在下面再做分析。 我們在這裡需要注意的點有兩個
      • 為了能夠將執行環境切換到主執行緒中,我們要求 sHandler 必須在主執行緒中建立,所以 AsyncTask 的建構函式必須在 UI 執行緒中呼叫
      • 我們知道靜態成員會在載入類的時候進行初始化,由於 sHandler 是個靜態變數,那麼我們要求 AsyncTask 類必須在 UI 執行緒中進行載入,否則 AsyncTask 無法正常工作
  • 初始化 mWorker

    • mWorker 是一個 Callable 物件,並在之後初始化 mFutrue 的時候作為引數傳入,我們在 mWorker 的程式碼實現這裡可以看到 doInBackground() 在這裡被呼叫了(但是真正的呼叫時機不不是在這裡進行的,而是在 SerialExecutor 的 execute 的方法中),並將結果返回給 result,並在方法結束之前呼叫 postResult(result)。 方法
  • 初始化 mFuture

    • MFuture 是一個 FutureTask 物件

execute() 中做了什麼

接著如果想要啟動某一個任務,就需要呼叫該任務的 execute() 方法,因此現在我們來看一看 execute() 方法的原始碼

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

execute() 方法的比較簡單,只是呼叫了 executeOnExecutor 方法,那麼具體的邏輯是在 executeOnExecutor 裡面,我們再接著看下去:

    @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;
    }

複製程式碼
  • 首先,executeOnExecutor 先對當前的狀態進行判斷,由於 AsyncTask 中有三個狀態(PENDING、RUNNING、FINISHED),並且每個狀態在 AsyncTask 的生命週期中有且只能被執行一次,如果當前的狀態不是 Status.PENDING(未執行),那麼就丟擲異常。在這裡也解釋了,我們 execute() 方法只能被執行一次,再次呼叫會報錯的原因。
  • 接著,我們可以看到 onPreExecute() 方法被呼叫了
  • 緊接著,我們可以看到 exec.execute(mFuture) 這行程式碼,而 exec 又是什麼,通過查詢上面的 execute 方法,我們可以看到這個 exec 是一個 sDefaultExecutor 變數,sDefaultExecutor 變數的定義如下:
    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

    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);
            }
        }
    }

複製程式碼

從以上程式碼可以看到,在 executeOnExecutor 中的 exce 最終呼叫的是 SerialExecutor 中 execute() 方法。而 execute() 這個方法的邏輯是在子執行緒中執行的,而 execute 這個方法傳入的 Runnable 正是 mFuture,在 run 方法中呼叫了, mFuture 物件的 run。我們再找到 FutureTask 中實現的 run 方法的程式碼,程式碼如下(有省略):

    public void run() {
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                try {
                    result = c.call();
                } catch (Throwable ex) {
                   
                }
            }
        } finally {
            // .....
        }
    }

複製程式碼

從上面的程式碼中我們可以看出,最終呼叫了 Callable 中的 call (),而這個 callball 就是我們在 executeOnExecutor 中傳入 mFuture 的 mWorker 物件。現在我們又重新拿出 MWorker 的程式碼來看一下:

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

我們從上面的程式碼可以看到兩個主要的點,首先是 doInBackground() 方法的呼叫,並將結果給到 result,最後在方法結束之前呼叫了 postResult(result);

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

而在 postResult 中使用了 sHandler 物件傳送了一條訊息,訊息中攜帶了 MESSAGE_POST_RESULT 常量和一個表示任務執行結果的 AsyncTaskResult 物件。這個 sHandler 物件是 InternalHandler 類的一個例項,那麼稍後這條訊息肯定會在 InternalHandler 的 handleMessage() 方法中被處理。InternalHandler 的原始碼如下所示:

    private static class InternalHandler extends Handler {
       
        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 訊息,就會去執行 finish() 方法,如果這是一條 MESSAGE_POST_PROGRESS 訊息,就會去執行 onProgressUpdate() 方法。那麼 finish() 方法的原始碼如下所示:

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

複製程式碼

可以看到,如果當前任務被取消掉了,就會呼叫 onCancelled() 方法,如果沒有被取消,則呼叫 onPostExecute() 方法,這樣當前任務的執行就全部結束了。

我們注意到,在剛才 InternalHandler 的handleMessage() 方法裡,還有一種 MESSAGE_POST_PROGRESS 的訊息型別,這種訊息是用於當前進度的,呼叫的正是 onProgressUpdate() 方法,那麼什麼時候才會發出這樣一條訊息呢?相信你已經猜到了,檢視publishProgress()方法的原始碼,如下所示:

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

最後我們來理一下 executeOnExecutor 中的任務啟動到結束,關鍵方法的呼叫順序:

executeOnExecutor() -> sDefaultExecutor.execute() -> mFuture.run() -> mWorker.call() -> doInBackground() -> postResult() -> sHandler.sendMessage() -> sHandler.handleMessage() -> onPostExecute()

複製程式碼

問題解決

  1. 為什麼 AsyncTask 的物件只能被呼叫一次,否則會出錯?(每個狀態只能執行一次)

    從上面我們知道,AsyncTask 有 3 個狀態,分別為 PENDING、RUNNING、FINSHED,而且每個狀態在 AsyncTask 的生命週期中有且只執行一次。由於在執行完 execute 方法的時候會先對 AsyncTask 的狀態進行判斷,如果是 PENDING(等待中)的狀態,就會往下執行並將 AsyncTask 的狀態設定為 RUNNING(執行中)的狀態;否則會丟擲錯誤。AsyncTask finish 的時候,AsyncTask 的狀態會被設定為 FINSHED 狀態。
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)");
            }
        }
複製程式碼
  1. AsyncTask 的類為什麼必須在主執行緒載入

    由於 sHandler 是一個靜態的 Handler 物件,為了能夠將執行環境切換到主執行緒中,這就要求 sHandler 必須在主執行緒中建立,也即是 AsyncTask 必須在主執行緒中建立。由於靜態成員會在載入類的時候進行初始化,因此也要求 AsyncTask 的類必須在主執行緒中載入,否則 AsyncTask 無法正常工作。

  2. AsyncTask 的物件為什麼必須在主執行緒中建立

    因為在 AsyncTask 的建構函式中對 handler 進行了初始化的操作,所以 AsyncTask 必須在主執行緒中進行建立,否則 AsyncTask 無法進行執行緒切換的工作

  3. AsyncTask 是序列執行任務還是並行執行任務?

    在 Android 1.6 之前,AsyncTask 是序列執行任務的,Android 1.6的時候 AsyncTask 是並行執行任務的,Android 3.0 之後,為了避免並行錯誤,AsyncTask 又採用一個執行緒來序列執行任務。

  4. AsyncTask 呼叫 cancel() 任務是否立即停止執行?onPostExecute() 還會被呼叫嗎?onCancelled() 什麼時候被呼叫?

    任務不會立即停止的,我們呼叫 cancel 的時候,只是將 AsyncTask 設定為 canceled(可取消)狀態,我們從以下程式碼可以看出,AsyncTask 設定為已取消的狀態,那麼之後 onProgressUpdate 和 onPostExecute 都不會被呼叫,而是呼叫了 onCancelled() 方法。onCancelled() 方法是在非同步任務結束的時候才呼叫的。時機是和 onPostExecute 方法一樣的,只是這兩個方法是互斥的,不能同時出現。

@WorkerThread
    protected final void publishProgress(Progress... values) {
        if (!isCancelled()) {
            getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                    new AsyncTaskResult<Progress>(this, values)).sendToTarget();
        }
    }
    
// 執行緒執行完之後才會被呼叫
private void finish(Result result) {
        if (isCancelled()) {
            onCancelled(result);
        } else {
            onPostExecute(result);
        }
        mStatus = Status.FINISHED;
    }
複製程式碼

AsyncTask 在使用的過程中的一些限制總結

  • (1)AsyncTask 的類必須在主執行緒載入
  • (2)AsyncTask 的物件必須在主執行緒中建立
  • (3)execute 方法必須在 UI 執行緒中呼叫
  • (4)不要在主執行緒中呼叫 onPreExecute 、doInbackground、onPressUpdate、onPostExecute
  • (5)AsyncTask 的物件只能被呼叫一次,否則會出錯
  • (6)AsyncTask 不太適合做太耗時的操作
  • (7)在 Android 1.6 之前,AsyncTask 是序列執行任務的,Android 1.6的時候 AsyncTask 是並行執行任務的,Android 3.0 之後,為了避免並行錯誤,AsyncTask 又採用一個執行緒來序列執行任務。
  • (8)如果在 AsyncTask 中的 doInBackGround 中開啟了新的執行緒,我們執行了 cancle() 方法來停止非同步任務,執行緒是不會被停止的,直到任務執行完成為止,這個過程中,onProgressUpdate 和 onPostExecute 是不會被呼叫的

相關文章