Android AsyncTask完全解析,帶你從原始碼的角度徹底理解

許佳佳233發表於2017-01-19

轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/11711405

 

我們都知道,Android UI是執行緒不安全的,如果想要在子執行緒裡進行UI操作,就需要藉助Android的非同步訊息處理機制。之前我也寫過了一篇文章從原始碼層面分析了Android的非同步訊息處理機制,感興趣的朋友可以參考 Android Handler、Message完全解析,帶你從原始碼的角度徹底理解 。

 

不過為了更加方便我們在子執行緒中更新UI元素,Android從1.5版本就引入了一個AsyncTask類,使用它就可以非常靈活方便地從子執行緒切換到UI執行緒,我們本篇文章的主角也就正是它了。

 

AsyncTask很早就出現在Android的API裡了,所以我相信大多數朋友對它的用法都已經非常熟悉。不過今天我還是準備從AsyncTask的基本用法開始講起,然後我們再來一起分析下AsyncTask原始碼,看看它是如何實現的,最後我會介紹一些關於AsyncTask你所不知道的祕密。

 

AsyncTask的基本用法

 

首先來看一下AsyncTask的基本用法,由於AsyncTask是一個抽象類,所以如果我們想使用它,就必須要建立一個子類去繼承它。在繼承時我們可以為AsyncTask類指定三個泛型引數,這三個引數的用途如下:

 

1. Params

在執行AsyncTask時需要傳入的引數,可用於在後臺任務中使用。

2. Progress

後臺任務執行時,如果需要在介面上顯示當前的進度,則使用這裡指定的泛型作為進度單位。

3. Result

當任務執行完畢後,如果需要對結果進行返回,則使用這裡指定的泛型作為返回值型別。

 

因此,一個最簡單的自定義AsyncTask就可以寫成如下方式:

[java] view plain copy

  1. class DownloadTask extends AsyncTask<Void, Integer, Boolean> {  
  2.     ……  
  3. }  

這裡我們把AsyncTask的第一個泛型引數指定為Void,表示在執行AsyncTask的時候不需要傳入引數給後臺任務。第二個泛型引數指定為Integer,表示使用整型資料來作為進度顯示單位。第三個泛型引數指定為Boolean,則表示使用布林型資料來反饋執行結果。

 

當然,目前我們自定義的DownloadTask還是一個空任務,並不能進行任何實際的操作,我們還需要去重寫AsyncTask中的幾個方法才能完成對任務的定製。經常需要去重寫的方法有以下四個:

 

1. onPreExecute()

這個方法會在後臺任務開始執行之間呼叫,用於進行一些介面上的初始化操作,比如顯示一個進度條對話方塊等。

2. doInBackground(Params...)

這個方法中的所有程式碼都會在子執行緒中執行,我們應該在這裡去處理所有的耗時任務。任務一旦完成就可以通過return語句來將任務的執行結果進行返回,如果AsyncTask的第三個泛型引數指定的是Void,就可以不返回任務執行結果。注意,在這個方法中是不可以進行UI操作的,如果需要更新UI元素,比如說反饋當前任務的執行進度,可以呼叫publishProgress(Progress...)方法來完成。

3. onProgressUpdate(Progress...)

當在後臺任務中呼叫了publishProgress(Progress...)方法後,這個方法就很快會被呼叫,方法中攜帶的引數就是在後臺任務中傳遞過來的。在這個方法中可以對UI進行操作,利用引數中的數值就可以對介面元素進行相應的更新。

4. onPostExecute(Result)

當後臺任務執行完畢並通過return語句進行返回時,這個方法就很快會被呼叫。返回的資料會作為引數傳遞到此方法中,可以利用返回的資料來進行一些UI操作,比如說提醒任務執行的結果,以及關閉掉進度條對話方塊等。

 

因此,一個比較完整的自定義AsyncTask就可以寫成如下方式:

[java] view plain copy

  1. class DownloadTask extends AsyncTask<Void, Integer, Boolean> {  
  2.   
  3.     @Override  
  4.     protected void onPreExecute() {  
  5.         progressDialog.show();  
  6.     }  
  7.   
  8.     @Override  
  9.     protected Boolean doInBackground(Void... params) {  
  10.         try {  
  11.             while (true) {  
  12.                 int downloadPercent = doDownload();  
  13.                 publishProgress(downloadPercent);  
  14.                 if (downloadPercent >= 100) {  
  15.                     break;  
  16.                 }  
  17.             }  
  18.         } catch (Exception e) {  
  19.             return false;  
  20.         }  
  21.         return true;  
  22.     }  
  23.   
  24.     @Override  
  25.     protected void onProgressUpdate(Integer... values) {  
  26.         progressDialog.setMessage("當前下載進度:" + values[0] + "%");  
  27.     }  
  28.   
  29.     @Override  
  30.     protected void onPostExecute(Boolean result) {  
  31.         progressDialog.dismiss();  
  32.         if (result) {  
  33.             Toast.makeText(context, "下載成功", Toast.LENGTH_SHORT).show();  
  34.         } else {  
  35.             Toast.makeText(context, "下載失敗", Toast.LENGTH_SHORT).show();  
  36.         }  
  37.     }  
  38. }  

這裡我們模擬了一個下載任務,在doInBackground()方法中去執行具體的下載邏輯,在onProgressUpdate()方法中顯示當前的下載進度,在onPostExecute()方法中來提示任務的執行結果。如果想要啟動這個任務,只需要簡單地呼叫以下程式碼即可:

[java] view plain copy

  1. new DownloadTask().execute();  

以上就是AsyncTask的基本用法,怎麼樣,是不是感覺在子執行緒和UI執行緒之間進行切換變得靈活了很多?我們並不需求去考慮什麼非同步訊息處理機制,也不需要專門使用一個Handler來傳送和接收訊息,只需要呼叫一下publishProgress()方法就可以輕鬆地從子執行緒切換到UI執行緒了。

 

分析AsyncTask的原始碼

 

雖然AsyncTask這麼簡單好用,但你知道它是怎樣實現的嗎?那麼接下來,我們就來分析一下AsyncTask的原始碼,對它的實現原理一探究竟。注意這裡我選用的是Android 4.0的原始碼,如果你檢視的是其它版本的原始碼,可能會有一些出入。

 

從之前DownloadTask的程式碼就可以看出,在啟動某一個任務之前,要先new出它的例項,因此,我們就先來看一看AsyncTask建構函式中的原始碼,如下所示:

[java] view plain copy

  1. public AsyncTask() {  
  2.     mWorker = new WorkerRunnable<Params, Result>() {  
  3.         public Result call() throws Exception {  
  4.             mTaskInvoked.set(true);  
  5.             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);  
  6.             return postResult(doInBackground(mParams));  
  7.         }  
  8.     };  
  9.     mFuture = new FutureTask<Result>(mWorker) {  
  10.         @Override  
  11.         protected void done() {  
  12.             try {  
  13.                 final Result result = get();  
  14.                 postResultIfNotInvoked(result);  
  15.             } catch (InterruptedException e) {  
  16.                 android.util.Log.w(LOG_TAG, e);  
  17.             } catch (ExecutionException e) {  
  18.                 throw new RuntimeException("An error occured while executing doInBackground()",  
  19.                         e.getCause());  
  20.             } catch (CancellationException e) {  
  21.                 postResultIfNotInvoked(null);  
  22.             } catch (Throwable t) {  
  23.                 throw new RuntimeException("An error occured while executing "  
  24.                         + "doInBackground()", t);  
  25.             }  
  26.         }  
  27.     };  
  28. }  

這段程式碼雖然看起來有點長,但實際上並沒有任何具體的邏輯會得到執行,只是初始化了兩個變數,mWorker和mFuture,並在初始化mFuture的時候將mWorker作為引數傳入。mWorker是一個Callable物件,mFuture是一個FutureTask物件,這兩個變數會暫時儲存在記憶體中,稍後才會用到它們。

 

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

[java] view plain copy

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

簡單的有點過分了,只有一行程式碼,僅是呼叫了executeOnExecutor()方法,那麼具體的邏輯就應該寫在這個方法裡了,快跟進去瞧一瞧:

[java] view plain copy

  1. public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,  
  2.         Params... params) {  
  3.     if (mStatus != Status.PENDING) {  
  4.         switch (mStatus) {  
  5.             case RUNNING:  
  6.                 throw new IllegalStateException("Cannot execute task:"  
  7.                         + " the task is already running.");  
  8.             case FINISHED:  
  9.                 throw new IllegalStateException("Cannot execute task:"  
  10.                         + " the task has already been executed "  
  11.                         + "(a task can be executed only once)");  
  12.         }  
  13.     }  
  14.     mStatus = Status.RUNNING;  
  15.     onPreExecute();  
  16.     mWorker.mParams = params;  
  17.     exec.execute(mFuture);  
  18.     return this;  
  19. }  

果然,這裡的程式碼看上去才正常點。可以看到,在第15行呼叫了onPreExecute()方法,因此證明了onPreExecute()方法會第一個得到執行。可是接下來的程式碼就看不明白了,怎麼沒見到哪裡有呼叫doInBackground()方法呢?彆著急,慢慢找總會找到的,我們看到,在第17行呼叫了Executor的execute()方法,並將前面初始化的mFuture物件傳了進去,那麼這個Executor物件又是什麼呢?檢視上面的execute()方法,原來是傳入了一個sDefaultExecutor變數,接著找一下這個sDefaultExecutor變數是在哪裡定義的,原始碼如下所示:

[java] view plain copy

  1. public static final Executor SERIAL_EXECUTOR = new SerialExecutor();  
  2. ……  
  3. private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;  

可以看到,這裡先new出了一個SERIAL_EXECUTOR常量,然後將sDefaultExecutor的值賦值為這個常量,也就是說明,剛才在executeOnExecutor()方法中呼叫的execute()方法,其實也就是呼叫的SerialExecutor類中的execute()方法。那麼我們自然要去看看SerialExecutor的原始碼了,如下所示:

[java] view plain copy

  1. private static class SerialExecutor implements Executor {  
  2.     final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();  
  3.     Runnable mActive;  
  4.   
  5.     public synchronized void execute(final Runnable r) {  
  6.         mTasks.offer(new Runnable() {  
  7.             public void run() {  
  8.                 try {  
  9.                     r.run();  
  10.                 } finally {  
  11.                     scheduleNext();  
  12.                 }  
  13.             }  
  14.         });  
  15.         if (mActive == null) {  
  16.             scheduleNext();  
  17.         }  
  18.     }  
  19.   
  20.     protected synchronized void scheduleNext() {  
  21.         if ((mActive = mTasks.poll()) != null) {  
  22.             THREAD_POOL_EXECUTOR.execute(mActive);  
  23.         }  
  24.     }  
  25. }  

SerialExecutor類中也有一個execute()方法,這個方法裡的所有邏輯就是在子執行緒中執行的了,注意這個方法有一個Runnable引數,那麼目前這個引數的值是什麼呢?當然就是mFuture物件了,也就是說在第9行我們要呼叫的是FutureTask類的run()方法,而在這個方法裡又會去呼叫Sync內部類的innerRun()方法,因此我們直接來看innerRun()方法的原始碼:

[java] view plain copy

  1. void innerRun() {  
  2.     if (!compareAndSetState(READY, RUNNING))  
  3.         return;  
  4.     runner = Thread.currentThread();  
  5.     if (getState() == RUNNING) { // recheck after setting thread  
  6.         V result;  
  7.         try {  
  8.             result = callable.call();  
  9.         } catch (Throwable ex) {  
  10.             setException(ex);  
  11.             return;  
  12.         }  
  13.         set(result);  
  14.     } else {  
  15.         releaseShared(0); // cancel  
  16.     }  
  17. }  

可以看到,在第8行呼叫了callable的call()方法,那麼這個callable物件是什麼呢?其實就是在初始化mFuture物件時傳入的mWorker物件了,此時呼叫的call()方法,也就是一開始在AsyncTask的建構函式中指定的,我們把它單獨拿出來看一下,程式碼如下所示:

[java] view plain copy

  1. public Result call() throws Exception {  
  2.     mTaskInvoked.set(true);  
  3.     Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);  
  4.     return postResult(doInBackground(mParams));  
  5. }  

在postResult()方法的引數裡面,我們終於找到了doInBackground()方法的呼叫處,雖然經過了很多週轉,但目前的程式碼仍然是執行在子執行緒當中的,所以這也就是為什麼我們可以在doInBackground()方法中去處理耗時的邏輯。接著將doInBackground()方法返回的結果傳遞給了postResult()方法,這個方法的原始碼如下所示:

[java] view plain copy

  1. private Result postResult(Result result) {  
  2.     Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,  
  3.             new AsyncTaskResult<Result>(this, result));  
  4.     message.sendToTarget();  
  5.     return result;  
  6. }  

如果你已經熟悉了非同步訊息處理機制,這段程式碼對你來說一定非常簡單吧。這裡使用sHandler物件發出了一條訊息,訊息中攜帶了MESSAGE_POST_RESULT常量和一個表示任務執行結果的AsyncTaskResult物件。這個sHandler物件是InternalHandler類的一個例項,那麼稍後這條訊息肯定會在InternalHandler的handleMessage()方法中被處理。InternalHandler的原始碼如下所示:

[java] view plain copy

  1. private static class InternalHandler extends Handler {  
  2.     @SuppressWarnings({"unchecked""RawUseOfParameterizedType"})  
  3.     @Override  
  4.     public void handleMessage(Message msg) {  
  5.         AsyncTaskResult result = (AsyncTaskResult) msg.obj;  
  6.         switch (msg.what) {  
  7.             case MESSAGE_POST_RESULT:  
  8.                 // There is only one result  
  9.                 result.mTask.finish(result.mData[0]);  
  10.                 break;  
  11.             case MESSAGE_POST_PROGRESS:  
  12.                 result.mTask.onProgressUpdate(result.mData);  
  13.                 break;  
  14.         }  
  15.     }  
  16. }  

這裡對訊息的型別進行了判斷,如果這是一條MESSAGE_POST_RESULT訊息,就會去執行finish()方法,如果這是一條MESSAGE_POST_PROGRESS訊息,就會去執行onProgressUpdate()方法。那麼finish()方法的原始碼如下所示:

[java] view plain copy

  1. private void finish(Result result) {  
  2.     if (isCancelled()) {  
  3.         onCancelled(result);  
  4.     } else {  
  5.         onPostExecute(result);  
  6.     }  
  7.     mStatus = Status.FINISHED;  
  8. }  

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

 

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

[java] view plain copy

  1. protected final void publishProgress(Progress... values) {  
  2.     if (!isCancelled()) {  
  3.         sHandler.obtainMessage(MESSAGE_POST_PROGRESS,  
  4.                 new AsyncTaskResult<Progress>(this, values)).sendToTarget();  
  5.     }  
  6. }  

非常清晰了吧!正因如此,在doInBackground()方法中呼叫publishProgress()方法才可以從子執行緒切換到UI執行緒,從而完成對UI元素的更新操作。其實也沒有什麼神祕的,因為說到底,AsyncTask也是使用的非同步訊息處理機制,只是做了非常好的封裝而已。

 

讀到這裡,相信你對AsyncTask中的每個回撥方法的作用、原理、以及何時會被呼叫都已經搞明白了吧。

 

關於AsyncTask你所不知道的祕密

 

不得不說,剛才我們在分析SerialExecutor的時候,其實並沒有分析的很仔細,僅僅只是關注了它會呼叫mFuture中的run()方法,但是至於什麼時候會呼叫我們並沒有進一步地研究。其實SerialExecutor也是AsyncTask在3.0版本以後做了最主要的修改的地方,它在AsyncTask中是以常量的形式被使用的,因此在整個應用程式中的所有AsyncTask例項都會共用同一個SerialExecutor。下面我們就來對這個類進行更加詳細的分析,為了方便閱讀,我把它的程式碼再貼出來一遍:

[java] view plain copy

  1. private static class SerialExecutor implements Executor {  
  2.     final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();  
  3.     Runnable mActive;  
  4.   
  5.     public synchronized void execute(final Runnable r) {  
  6.         mTasks.offer(new Runnable() {  
  7.             public void run() {  
  8.                 try {  
  9.                     r.run();  
  10.                 } finally {  
  11.                     scheduleNext();  
  12.                 }  
  13.             }  
  14.         });  
  15.         if (mActive == null) {  
  16.             scheduleNext();  
  17.         }  
  18.     }  
  19.   
  20.     protected synchronized void scheduleNext() {  
  21.         if ((mActive = mTasks.poll()) != null) {  
  22.             THREAD_POOL_EXECUTOR.execute(mActive);  
  23.         }  
  24.     }  
  25. }  

可以看到,SerialExecutor是使用ArrayDeque這個佇列來管理Runnable物件的,如果我們一次性啟動了很多個任務,首先在第一次執行execute()方法的時候,會呼叫ArrayDeque的offer()方法將傳入的Runnable物件新增到佇列的尾部,然後判斷mActive物件是不是等於null,第一次執行當然是等於null了,於是會呼叫scheduleNext()方法。在這個方法中會從佇列的頭部取值,並賦值給mActive物件,然後呼叫THREAD_POOL_EXECUTOR去執行取出的取出的Runnable物件。之後如何又有新的任務被執行,同樣還會呼叫offer()方法將傳入的Runnable新增到佇列的尾部,但是再去給mActive物件做非空檢查的時候就會發現mActive物件已經不再是null了,於是就不會再呼叫scheduleNext()方法。

 

那麼後面新增的任務豈不是永遠得不到處理了?當然不是,看一看offer()方法裡傳入的Runnable匿名類,這裡使用了一個try finally程式碼塊,並在finally中呼叫了scheduleNext()方法,保證無論發生什麼情況,這個方法都會被呼叫。也就是說,每次當一個任務執行完畢後,下一個任務才會得到執行,SerialExecutor模仿的是單一執行緒池的效果,如果我們快速地啟動了很多工,同一時刻只會有一個執行緒正在執行,其餘的均處於等待狀態。Android照片牆應用實現,再多的圖片也不怕崩潰 這篇文章中例子的執行結果也證實了這個結論。

 

不過你可能還不知道,在Android 3.0之前是並沒有SerialExecutor這個類的,那個時候是直接在AsyncTask中構建了一個sExecutor常量,並對執行緒池總大小,同一時刻能夠執行的執行緒數做了規定,程式碼如下所示:

[java] view plain copy

  1. private static final int CORE_POOL_SIZE = 5;  
  2. private static final int MAXIMUM_POOL_SIZE = 128;  
  3. private static final int KEEP_ALIVE = 10;  
  4. ……  
  5. private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,  
  6.         MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory);  

可以看到,這裡規定同一時刻能夠執行的執行緒數為5個,執行緒池總大小為128。也就是說當我們啟動了10個任務時,只有5個任務能夠立刻執行,另外的5個任務則需要等待,當有一個任務執行完畢後,第6個任務才會啟動,以此類推。而執行緒池中最大能存放的執行緒數是128個,當我們嘗試去新增第129個任務時,程式就會崩潰。

 

因此在3.0版本中AsyncTask的改動還是挺大的,在3.0之前的AsyncTask可以同時有5個任務在執行,而3.0之後的AsyncTask同時只能有1個任務在執行。為什麼升級之後可以同時執行的任務數反而變少了呢?這是因為更新後的AsyncTask已變得更加靈活,如果不想使用預設的執行緒池,還可以自由地進行配置。比如使用如下的程式碼來啟動任務:

[java] view plain copy

  1. Executor exec = new ThreadPoolExecutor(1520010,  
  2.         TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());  
  3. new DownloadTask().executeOnExecutor(exec);  

這樣就可以使用我們自定義的一個Executor來執行任務,而不是使用SerialExecutor。上述程式碼的效果允許在同一時刻有15個任務正在執行,並且最多能夠儲存200個任務。

 

好了,到這裡我們就已經把關於AsyncTask的所有重要內容深入淺出地理解了一遍,相信在將來使用它的時候能夠更加得心應手。

相關文章