雖說現在做網絡請求有了Volley全家桶和OkHttp這樣好用的庫,但是在處理其他后臺任務以及與UI交互上,還是需要用到AsyncTask。但是你真的了解AsyncTask嗎?
AsyncTask的實現幾經修改,因此在不同版本的Android系統上表現各異;我相信,任何一個用戶量上千萬的產品絕對不會在代碼里面使用系統原生的AsynTask,因為它蛋疼的兼容性以及極高的崩潰率實在讓人不敢恭維。本文將帶你了解AsyncTask背后的原理,并給出一個久經考驗的AsyncTask修改版。
AsyncTask是什么?
AsyncTask到底是什么呢?很簡單,它不過是對線程池和Handler的封裝;用線程池來處理后臺任務,用Handler來處理與UI的交互。線程池使用的是Executor
接口,我們先了解一下線程池的特性。
線程池ThreadPoolExecutor
JDK5帶來的一大改進就是Java的并發能力,它提供了三種并發武器:并發框架Executor,并發集合類型如ConcurrentHashMap,并發控制類如CountDownLatch等;圣經《Effective Java》也說,盡量使用Exector而不是直接用Thread類進行并發編程。
AsyncTask內部也使用了線程池處理并發;線程池通過ThreadPoolExector
類構造,這個構造函數參數比較多,它允許開發者對線程池進行定制,我們先看看這每個參數是什么意思,然后看看Android是以何種方式定制的。
ThreadPoolExecutor的其他構造函數最終都會調用如下的構造函數完成對象創建工作:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize: 核心線程數目,即使線程池沒有任務,核心線程也不會終止(除非設置了allowCoreThreadTimeOut參數)可以理解為“常駐線程”
- maximumPoolSize: 線程池中允許的最大線程數目;一般來說,線程越多,線程調度開銷越大;因此一般都有這個限制。
- keepAliveTime: 當線程池中的線程數目比核心線程多的時候,如果超過這個keepAliveTime的時間,多余的線程會被回收;這些與核心線程相對的線程通常被稱為緩存線程
- unit: keepAliveTime的時間單位
- workQueue: 任務執行前保存任務的隊列;這個隊列僅保存由execute提交的Runnable任務
- threadFactory: 用來構造線程池的工廠;一般都是使用默認的;
- handler: 當線程池由于線程數目和隊列限制而導致后續任務阻塞的時候,線程池的處理方式。
那么,當一個新的任務到達的時候,線程池中的線程是如何調度的呢?(別慌,講這么一大段線程池的知識,是為了理解AsyncTask;Be Patient)
- 如果線程池中線程的數目少于corePoolSize,就算線程池中有其他的沒事做的核心線程,線程池還是會重新創建一個核心線程;直到核心線程數目到達corePoolSize(常駐線程就位)
- 如果線程池中線程的數目大于或者等于corePoolSize,但是工作隊列workQueue沒有滿,那么新的任務會放在隊列workQueue中,按照FIFO的原則依次等待執行;(當有核心線程處理完任務空閑出來后,會檢查這個工作隊列然后取出任務默默執行去)
- 如果線程池中線程數目大于等于corePoolSize,并且工作隊列workQueue滿了,但是總線程數目小于maximumPoolSize,那么直接創建一個線程處理被添加的任務。
- 如果工作隊列滿了,并且線程池中線程的數目到達了最大數目maximumPoolSize,那么就會用最后一個構造參數
handler
處理;**默認的處理方式是直接丟掉任務,然后拋出一個異常。
總結起來,也即是說,當有新的任務要處理時,先看線程池中的線程數量是否大于 corePoolSize,再看緩沖隊列 workQueue 是否滿,最后看線程池中的線程數量是否大于 maximumPoolSize。另外,當線程池中的線程數量大于 corePoolSize 時,如果里面有線程的空閑時間超過了 keepAliveTime,就將其移除線程池,這樣,可以動態地調整線程池中線程的數量。
我們以API 22為例,看一看AsyncTask里面的線程池是以什么參數構造的;AsyncTask里面有“兩個”線程池;一個THREAD_POOL_EXECUTOR
一個SERIAL_EXECUTOR
;之所以打引號,是因為其實SERIAL_EXECUTOR
也使用THREAD_POOL_EXECUTOR
實現的,只不過加了一個隊列弄成了串行而已,那么這個THREAD_POOL_EXECUTOR
是如何構造的呢?
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE = 1;
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
可以看到,AsyncTask里面線程池是一個核心線程數為CPU + 1
,最大線程數為CPU * 2 + 1
,工作隊列長度為128的線程池;并且沒有傳遞handler
參數,那么使用的就是默認的Handler(拒絕執行).
那么問題來了:
如果任務過多,那么超過了工作隊列以及線程數目的限制導致這個線程池發生阻塞,那么悲劇發生,默認的處理方式會直接拋出一個異常導致進程掛掉。假設你自己寫一個異步圖片加載的框架,然后用AsyncTask實現的話,當你快速滑動ListView的時候很容易發生這種異常;這也是為什么各大ImageLoader都是自己寫線程池和Handlder的原因。
這個線程池是一個靜態變量;那么在同一個進程之內,所有地方使用到的AsyncTask默認構造函數構造出來的AsyncTask都使用的是同一個線程池,如果App模塊比較多并且不加控制的話,很容易滿足第一條的崩潰條件;如果你不幸在不同的AsyncTask的doInBackgroud里面訪問了共享資源,那么就會發生各種并發編程問題。
在AsyncTask全部執行完畢之后,進程中還是會常駐corePoolSize個線程;在Android 4.4 (API 19)以下,這個corePoolSize是hardcode的,數值是5;API 19改成了
cpu + 1
;也就是說,在Android 4.4以前;如果你執行了超過五個AsyncTask;然后啥也不干了,進程中還是會有5個AsyncTask線程;不信,你看:
Handler
AsyncTask里面的handler很簡單,如下(API 22代碼):
private static final InternalHandler sHandler = new InternalHandler();
public InternalHandler() {
super(Looper.getMainLooper());
}
注意,這里直接用的主線程的Looper;如果去看API 22以下的代碼,會發現它沒有這個構造函數,而是使用默認的;默認情況下,Handler會使用當前線程的Looper,如果你的AsyncTask是在子線程創建的,那么很不幸,你的onPreExecute
和onPostExecute
并非在UI線程執行,而是被Handler post到創建它的那個線程執行;如果你在這兩個線程更新了UI,那么直接導致崩潰。這也是大家口口相傳的AsyncTask必須在主線程創建的原因。
另外,AsyncTask里面的這個Handler是一個靜態變量,也就是說它是在類加載的時候創建的;如果在你的APP進程里面,以前從來沒有使用過AsyncTask,然后在子線程使用AsyncTask的相關變量,那么導致靜態Handler初始化,如果在API 16以下,那么會出現上面同樣的問題;這就是AsyncTask必須在主線程初始化 的原因。
事實上,在Android 4.1(API 16)以后,在APP主線程ActivityThread的main函數里面,直接調用了AscynTask.init
函數確保這個類是在主線程初始化的;另外,init這個函數里面獲取了InternalHandler
的Looper,由于是在主線程執行的,因此,AsyncTask的Handler用的也是主線程的Looper。這個問題從而得到徹底的解決。
AsyncTask是并行執行的嗎?
現在知道AsyncTask內部有一個線程池,那么派發給AsyncTask的任務是并行執行的嗎?
答案是不確定。在Android 1.5剛引入的時候,AsyncTask的execute
是串行執行的;到了Android 1.6直到Android 2.3.2,又被修改為并行執行了,這個執行任務的線程池就是THREAD_POOL_EXECUTOR
,因此在一個進程內,所有的AsyncTask都是并行執行的;但是在Android 3.0以后,如果你使用execute
函數直接執行AsyncTask,那么這些任務是串行執行的;(你說蛋疼不)源代碼如下:
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
這個sDefaultExecutor
就是用來執行任務的線程池,那么它的值是什么呢?繼續看代碼:
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
因此結論就來了:Android 3.0以上,AsyncTask默認并不是并行執行的;
為什么默認不并行執行?
也許你不理解,為什么AsyncTask默認把它設計為串行執行的呢?
由于一個進程內所有的AsyncTask都是使用的同一個線程池執行任務;如果同時有幾個AsyncTask一起并行執行的話,恰好AysncTask的使用者在doInbackgroud
里面訪問了相同的資源,但是自己沒有處理同步問題;那么就有可能導致災難性的后果!
由于開發者通常不會意識到需要對他們創建的所有的AsyncTask對象里面的doInbackgroud
做同步處理,因此,API的設計者為了避免這種無意中訪問并發資源的問題,干脆把這個API設置為默認所有串行執行的了。如果你明確知道自己需要并行處理任務,那么你需要使用executeOnExecutor(Executor exec,Params... params)
這個函數來指定你用來執行任務的線程池,同時為自己的行為負責。(處理同步問題)
實際上《Effective Java》里面有一條原則說的就是這種情況:不要在同步塊里面調用不可信的外來函數。這里明顯違背了這個原則:AsyncTask這個類并不知道使用者會在doInBackgroud
這個函數里面做什么,但是對它的行為做了某種假設。
如何讓AsyncTask并行執行?
正如上面所說,如果你確定自己做好了同步處理,或者你沒有在不同的AsyncTask里面訪問共享資源,需要AsyncTask能夠并行處理任務的話,你可以用帶有兩個參數的executeOnExecutor
執行任務:
new AsyncTask<Void, Void, Vo
@Override
protected Void doInBackground(Void... params) {
// do something
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
更好的AsyncTask
從上面的分析得知,AsyncTask有如下問題:
- 默認的AsyncTask如果處理的任務過多,會導致程序直接崩潰;
- AsyncTask類必須在主線程初始化,必須在主線程創建,不然在API 16以下很大概率崩潰。
- 如果你曾經使用過AsyncTask,以后不用了;在Android 4.4以下,進程內也默認有5個AsyncTask線程;在Android 4.4以上,默認有
CPU + 1
個線程。 - Android 3.0以上的AsyncTask默認是串行執行任務的;如果要并行執行需要調用低版本沒有的API,處理麻煩。
因此我們對系統的AsyncTask做了一些修改,在不同Android版本提供一致的行為,并且提高了使用此類的安全性,主要改動如下:
- 添加對于任務過多導致崩潰的異常保護;在這里進行必要的數據統計上報工作;如果出現這個問題,說明AsyncTask不適合這種場景了,需要考慮重構;
- 移植API 22對于Handler的處理;這樣就算在線程創建異步任務,也不會有任何問題;
- 提供串行執行和并行執行的
execute
方法;默認串行執行,如果明確知道自己在干什么,可以使用executeParallel
并行執行。 - 在
doInbackgroud
里面頻繁崩潰的地方加上try..catch
;自己處理數據上報工作。
完整代碼見gist,BetterAsyncTask