你真的了解AsyncTask?

雖說現在做網絡請求有了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)

  1. 如果線程池中線程的數目少于corePoolSize,就算線程池中有其他的沒事做的核心線程,線程池還是會重新創建一個核心線程;直到核心線程數目到達corePoolSize(常駐線程就位)
  2. 如果線程池中線程的數目大于或者等于corePoolSize,但是工作隊列workQueue沒有滿,那么新的任務會放在隊列workQueue中,按照FIFO的原則依次等待執行;(當有核心線程處理完任務空閑出來后,會檢查這個工作隊列然后取出任務默默執行去)
  3. 如果線程池中線程數目大于等于corePoolSize,并且工作隊列workQueue滿了,但是總線程數目小于maximumPoolSize,那么直接創建一個線程處理被添加的任務。
  4. 如果工作隊列滿了,并且線程池中線程的數目到達了最大數目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(拒絕執行).

那么問題來了:

  1. 如果任務過多,那么超過了工作隊列以及線程數目的限制導致這個線程池發生阻塞,那么悲劇發生,默認的處理方式會直接拋出一個異常導致進程掛掉。假設你自己寫一個異步圖片加載的框架,然后用AsyncTask實現的話,當你快速滑動ListView的時候很容易發生這種異常;這也是為什么各大ImageLoader都是自己寫線程池和Handlder的原因。

  2. 這個線程池是一個靜態變量;那么在同一個進程之內,所有地方使用到的AsyncTask默認構造函數構造出來的AsyncTask都使用的是同一個線程池,如果App模塊比較多并且不加控制的話,很容易滿足第一條的崩潰條件;如果你不幸在不同的AsyncTask的doInBackgroud里面訪問了共享資源,那么就會發生各種并發編程問題。

  3. 在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是在子線程創建的,那么很不幸,你的onPreExecuteonPostExecute并非在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有如下問題:

  1. 默認的AsyncTask如果處理的任務過多,會導致程序直接崩潰;
  2. AsyncTask類必須在主線程初始化,必須在主線程創建,不然在API 16以下很大概率崩潰。
  3. 如果你曾經使用過AsyncTask,以后不用了;在Android 4.4以下,進程內也默認有5個AsyncTask線程;在Android 4.4以上,默認有CPU + 1個線程。
  4. Android 3.0以上的AsyncTask默認是串行執行任務的;如果要并行執行需要調用低版本沒有的API,處理麻煩。

因此我們對系統的AsyncTask做了一些修改,在不同Android版本提供一致的行為,并且提高了使用此類的安全性,主要改動如下:

  1. 添加對于任務過多導致崩潰的異常保護;在這里進行必要的數據統計上報工作;如果出現這個問題,說明AsyncTask不適合這種場景了,需要考慮重構;
  2. 移植API 22對于Handler的處理;這樣就算在線程創建異步任務,也不會有任何問題;
  3. 提供串行執行和并行執行的execute方法;默認串行執行,如果明確知道自己在干什么,可以使用executeParallel并行執行。
  4. doInbackgroud里面頻繁崩潰的地方加上try..catch;自己處理數據上報工作。

完整代碼見gist,BetterAsyncTask

原文地址:http://weishu.me/2016/01/18/dive-into-asynctask/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,106評論 2 375

推薦閱讀更多精彩內容

  • 美圖欣賞 Java、Android知識點匯集 Java集合類 ** Java集合相關的博客** java面試相關 ...
    ElvenShi閱讀 1,761評論 0 2
  • 最近找實習面試中經常會被問到關于AsyncTask的一些內部機制的問題,之前也早有學習,但是還不夠系統,沒有形成一...
    水煮米茶閱讀 2,851評論 0 6
  • 前段時間遇到這樣一個問題,有人問微信朋友圈的上傳圖片的功能怎么做才能讓用戶的等待時間較短,比如說一下上傳9張圖片,...
    加油碼農閱讀 1,213評論 0 2
  • 前言## 任何一個Android 開發者對AsnycTask 都應該不陌生;使用AsyncTask可以很方便的異步...
    IAM四十二閱讀 1,422評論 3 18
  • 在老家陽光總是來得那么早,每天不到8點,金黃的陽光已經透過窗戶鋪滿了整個房間。其實我不是一個矯情的人,但是每次看到...
    overall閱讀 403評論 0 0