你真的了解AsyncTask?

雖說(shuō)現(xiàn)在做網(wǎng)絡(luò)請(qǐng)求有了Volley全家桶和OkHttp這樣好用的庫(kù),但是在處理其他后臺(tái)任務(wù)以及與UI交互上,還是需要用到AsyncTask。但是你真的了解AsyncTask嗎?

AsyncTask的實(shí)現(xiàn)幾經(jīng)修改,因此在不同版本的Android系統(tǒng)上表現(xiàn)各異;我相信,任何一個(gè)用戶(hù)量上千萬(wàn)的產(chǎn)品絕對(duì)不會(huì)在代碼里面使用系統(tǒng)原生的AsynTask,因?yàn)樗疤鄣募嫒菪砸约皹O高的崩潰率實(shí)在讓人不敢恭維。本文將帶你了解AsyncTask背后的原理,并給出一個(gè)久經(jīng)考驗(yàn)的AsyncTask修改版。

AsyncTask是什么?

AsyncTask到底是什么呢?很簡(jiǎn)單,它不過(guò)是對(duì)線(xiàn)程池和Handler的封裝;用線(xiàn)程池來(lái)處理后臺(tái)任務(wù),用Handler來(lái)處理與UI的交互。線(xiàn)程池使用的是Executor接口,我們先了解一下線(xiàn)程池的特性。

線(xiàn)程池ThreadPoolExecutor

JDK5帶來(lái)的一大改進(jìn)就是Java的并發(fā)能力,它提供了三種并發(fā)武器:并發(fā)框架Executor,并發(fā)集合類(lèi)型如ConcurrentHashMap,并發(fā)控制類(lèi)如CountDownLatch等;圣經(jīng)《Effective Java》也說(shuō),盡量使用Exector而不是直接用Thread類(lèi)進(jìn)行并發(fā)編程。

AsyncTask內(nèi)部也使用了線(xiàn)程池處理并發(fā);線(xiàn)程池通過(guò)ThreadPoolExector類(lèi)構(gòu)造,這個(gè)構(gòu)造函數(shù)參數(shù)比較多,它允許開(kāi)發(fā)者對(duì)線(xiàn)程池進(jìn)行定制,我們先看看這每個(gè)參數(shù)是什么意思,然后看看Android是以何種方式定制的。

ThreadPoolExecutor的其他構(gòu)造函數(shù)最終都會(huì)調(diào)用如下的構(gòu)造函數(shù)完成對(duì)象創(chuàng)建工作:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize: 核心線(xiàn)程數(shù)目,即使線(xiàn)程池沒(méi)有任務(wù),核心線(xiàn)程也不會(huì)終止(除非設(shè)置了allowCoreThreadTimeOut參數(shù))可以理解為“常駐線(xiàn)程”
  • maximumPoolSize: 線(xiàn)程池中允許的最大線(xiàn)程數(shù)目;一般來(lái)說(shuō),線(xiàn)程越多,線(xiàn)程調(diào)度開(kāi)銷(xiāo)越大;因此一般都有這個(gè)限制。
  • keepAliveTime: 當(dāng)線(xiàn)程池中的線(xiàn)程數(shù)目比核心線(xiàn)程多的時(shí)候,如果超過(guò)這個(gè)keepAliveTime的時(shí)間,多余的線(xiàn)程會(huì)被回收;這些與核心線(xiàn)程相對(duì)的線(xiàn)程通常被稱(chēng)為緩存線(xiàn)程
  • unit: keepAliveTime的時(shí)間單位
  • workQueue: 任務(wù)執(zhí)行前保存任務(wù)的隊(duì)列;這個(gè)隊(duì)列僅保存由execute提交的Runnable任務(wù)
  • threadFactory: 用來(lái)構(gòu)造線(xiàn)程池的工廠(chǎng);一般都是使用默認(rèn)的;
  • handler: 當(dāng)線(xiàn)程池由于線(xiàn)程數(shù)目和隊(duì)列限制而導(dǎo)致后續(xù)任務(wù)阻塞的時(shí)候,線(xiàn)程池的處理方式。

那么,當(dāng)一個(gè)新的任務(wù)到達(dá)的時(shí)候,線(xiàn)程池中的線(xiàn)程是如何調(diào)度的呢?(別慌,講這么一大段線(xiàn)程池的知識(shí),是為了理解AsyncTask;Be Patient)

  1. 如果線(xiàn)程池中線(xiàn)程的數(shù)目少于corePoolSize,就算線(xiàn)程池中有其他的沒(méi)事做的核心線(xiàn)程,線(xiàn)程池還是會(huì)重新創(chuàng)建一個(gè)核心線(xiàn)程;直到核心線(xiàn)程數(shù)目到達(dá)corePoolSize(常駐線(xiàn)程就位)
  2. 如果線(xiàn)程池中線(xiàn)程的數(shù)目大于或者等于corePoolSize,但是工作隊(duì)列workQueue沒(méi)有滿(mǎn),那么新的任務(wù)會(huì)放在隊(duì)列workQueue中,按照FIFO的原則依次等待執(zhí)行;(當(dāng)有核心線(xiàn)程處理完任務(wù)空閑出來(lái)后,會(huì)檢查這個(gè)工作隊(duì)列然后取出任務(wù)默默執(zhí)行去)
  3. 如果線(xiàn)程池中線(xiàn)程數(shù)目大于等于corePoolSize,并且工作隊(duì)列workQueue滿(mǎn)了,但是總線(xiàn)程數(shù)目小于maximumPoolSize,那么直接創(chuàng)建一個(gè)線(xiàn)程處理被添加的任務(wù)。
  4. 如果工作隊(duì)列滿(mǎn)了,并且線(xiàn)程池中線(xiàn)程的數(shù)目到達(dá)了最大數(shù)目maximumPoolSize,那么就會(huì)用最后一個(gè)構(gòu)造參數(shù)handler處理;**默認(rèn)的處理方式是直接丟掉任務(wù),然后拋出一個(gè)異常。

總結(jié)起來(lái),也即是說(shuō),當(dāng)有新的任務(wù)要處理時(shí),先看線(xiàn)程池中的線(xiàn)程數(shù)量是否大于 corePoolSize,再看緩沖隊(duì)列 workQueue 是否滿(mǎn),最后看線(xiàn)程池中的線(xiàn)程數(shù)量是否大于 maximumPoolSize。另外,當(dāng)線(xiàn)程池中的線(xiàn)程數(shù)量大于 corePoolSize 時(shí),如果里面有線(xiàn)程的空閑時(shí)間超過(guò)了 keepAliveTime,就將其移除線(xiàn)程池,這樣,可以動(dòng)態(tài)地調(diào)整線(xiàn)程池中線(xiàn)程的數(shù)量。

風(fēng)景

我們以API 22為例,看一看AsyncTask里面的線(xiàn)程池是以什么參數(shù)構(gòu)造的;AsyncTask里面有“兩個(gè)”線(xiàn)程池;一個(gè)THREAD_POOL_EXECUTOR一個(gè)SERIAL_EXECUTOR;之所以打引號(hào),是因?yàn)槠鋵?shí)SERIAL_EXECUTOR也使用THREAD_POOL_EXECUTOR實(shí)現(xiàn)的,只不過(guò)加了一個(gè)隊(duì)列弄成了串行而已,那么這個(gè)THREAD_POOL_EXECUTOR是如何構(gòu)造的呢?

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里面線(xiàn)程池是一個(gè)核心線(xiàn)程數(shù)為CPU + 1,最大線(xiàn)程數(shù)為CPU * 2 + 1,工作隊(duì)列長(zhǎng)度為128的線(xiàn)程池;并且沒(méi)有傳遞handler參數(shù),那么使用的就是默認(rèn)的Handler(拒絕執(zhí)行).

那么問(wèn)題來(lái)了:

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

  2. 這個(gè)線(xiàn)程池是一個(gè)靜態(tài)變量;那么在同一個(gè)進(jìn)程之內(nèi),所有地方使用到的AsyncTask默認(rèn)構(gòu)造函數(shù)構(gòu)造出來(lái)的AsyncTask都使用的是同一個(gè)線(xiàn)程池,如果App模塊比較多并且不加控制的話(huà),很容易滿(mǎn)足第一條的崩潰條件;如果你不幸在不同的AsyncTask的doInBackgroud里面訪(fǎng)問(wèn)了共享資源,那么就會(huì)發(fā)生各種并發(fā)編程問(wèn)題。

  3. 在AsyncTask全部執(zhí)行完畢之后,進(jìn)程中還是會(huì)常駐corePoolSize個(gè)線(xiàn)程;在Android 4.4 (API 19)以下,這個(gè)corePoolSize是hardcode的,數(shù)值是5;API 19改成了cpu + 1;也就是說(shuō),在Android 4.4以前;如果你執(zhí)行了超過(guò)五個(gè)AsyncTask;然后啥也不干了,進(jìn)程中還是會(huì)有5個(gè)AsyncTask線(xiàn)程;不信,你看:

Handler

AsyncTask里面的handler很簡(jiǎn)單,如下(API 22代碼):

private static final InternalHandler sHandler = new InternalHandler();

public InternalHandler() {
    super(Looper.getMainLooper());
}

注意,這里直接用的主線(xiàn)程的Looper;如果去看API 22以下的代碼,會(huì)發(fā)現(xiàn)它沒(méi)有這個(gè)構(gòu)造函數(shù),而是使用默認(rèn)的;默認(rèn)情況下,Handler會(huì)使用當(dāng)前線(xiàn)程的Looper,如果你的AsyncTask是在子線(xiàn)程創(chuàng)建的,那么很不幸,你的onPreExecuteonPostExecute并非在UI線(xiàn)程執(zhí)行,而是被Handler post到創(chuàng)建它的那個(gè)線(xiàn)程執(zhí)行;如果你在這兩個(gè)線(xiàn)程更新了UI,那么直接導(dǎo)致崩潰。這也是大家口口相傳的AsyncTask必須在主線(xiàn)程創(chuàng)建的原因。

另外,AsyncTask里面的這個(gè)Handler是一個(gè)靜態(tài)變量,也就是說(shuō)它是在類(lèi)加載的時(shí)候創(chuàng)建的;如果在你的APP進(jìn)程里面,以前從來(lái)沒(méi)有使用過(guò)AsyncTask,然后在子線(xiàn)程使用AsyncTask的相關(guān)變量,那么導(dǎo)致靜態(tài)Handler初始化,如果在API 16以下,那么會(huì)出現(xiàn)上面同樣的問(wèn)題;這就是AsyncTask必須在主線(xiàn)程初始化 的原因。

事實(shí)上,在Android 4.1(API 16)以后,在APP主線(xiàn)程ActivityThread的main函數(shù)里面,直接調(diào)用了AscynTask.init函數(shù)確保這個(gè)類(lèi)是在主線(xiàn)程初始化的;另外,init這個(gè)函數(shù)里面獲取了InternalHandler的Looper,由于是在主線(xiàn)程執(zhí)行的,因此,AsyncTask的Handler用的也是主線(xiàn)程的Looper。這個(gè)問(wèn)題從而得到徹底的解決。

AsyncTask是并行執(zhí)行的嗎?

現(xiàn)在知道AsyncTask內(nèi)部有一個(gè)線(xiàn)程池,那么派發(fā)給AsyncTask的任務(wù)是并行執(zhí)行的嗎?

答案是不確定。在Android 1.5剛引入的時(shí)候,AsyncTask的execute是串行執(zhí)行的;到了Android 1.6直到Android 2.3.2,又被修改為并行執(zhí)行了,這個(gè)執(zhí)行任務(wù)的線(xiàn)程池就是THREAD_POOL_EXECUTOR,因此在一個(gè)進(jìn)程內(nèi),所有的AsyncTask都是并行執(zhí)行的;但是在Android 3.0以后,如果你使用execute函數(shù)直接執(zhí)行AsyncTask,那么這些任務(wù)是串行執(zhí)行的;(你說(shuō)蛋疼不)源代碼如下:

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

這個(gè)sDefaultExecutor就是用來(lái)執(zhí)行任務(wù)的線(xiàn)程池,那么它的值是什么呢?繼續(xù)看代碼:

private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

因此結(jié)論就來(lái)了:Android 3.0以上,AsyncTask默認(rèn)并不是并行執(zhí)行的;

為什么默認(rèn)不并行執(zhí)行?

也許你不理解,為什么AsyncTask默認(rèn)把它設(shè)計(jì)為串行執(zhí)行的呢?

由于一個(gè)進(jìn)程內(nèi)所有的AsyncTask都是使用的同一個(gè)線(xiàn)程池執(zhí)行任務(wù);如果同時(shí)有幾個(gè)AsyncTask一起并行執(zhí)行的話(huà),恰好AysncTask的使用者在doInbackgroud里面訪(fǎng)問(wèn)了相同的資源,但是自己沒(méi)有處理同步問(wèn)題;那么就有可能導(dǎo)致災(zāi)難性的后果!

由于開(kāi)發(fā)者通常不會(huì)意識(shí)到需要對(duì)他們創(chuàng)建的所有的AsyncTask對(duì)象里面的doInbackgroud做同步處理,因此,API的設(shè)計(jì)者為了避免這種無(wú)意中訪(fǎng)問(wèn)并發(fā)資源的問(wèn)題,干脆把這個(gè)API設(shè)置為默認(rèn)所有串行執(zhí)行的了。如果你明確知道自己需要并行處理任務(wù),那么你需要使用executeOnExecutor(Executor exec,Params... params)這個(gè)函數(shù)來(lái)指定你用來(lái)執(zhí)行任務(wù)的線(xiàn)程池,同時(shí)為自己的行為負(fù)責(zé)。(處理同步問(wèn)題)

實(shí)際上《Effective Java》里面有一條原則說(shuō)的就是這種情況:不要在同步塊里面調(diào)用不可信的外來(lái)函數(shù)。這里明顯違背了這個(gè)原則:AsyncTask這個(gè)類(lèi)并不知道使用者會(huì)在doInBackgroud這個(gè)函數(shù)里面做什么,但是對(duì)它的行為做了某種假設(shè)。

如何讓AsyncTask并行執(zhí)行?

正如上面所說(shuō),如果你確定自己做好了同步處理,或者你沒(méi)有在不同的AsyncTask里面訪(fǎng)問(wèn)共享資源,需要AsyncTask能夠并行處理任務(wù)的話(huà),你可以用帶有兩個(gè)參數(shù)的executeOnExecutor執(zhí)行任務(wù):

new AsyncTask<Void, Void, Vo
    @Override
    protected Void doInBackground(Void... params) {
        // do something
        return null;
    }
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

更好的AsyncTask

從上面的分析得知,AsyncTask有如下問(wèn)題:

  1. 默認(rèn)的AsyncTask如果處理的任務(wù)過(guò)多,會(huì)導(dǎo)致程序直接崩潰;
  2. AsyncTask類(lèi)必須在主線(xiàn)程初始化,必須在主線(xiàn)程創(chuàng)建,不然在API 16以下很大概率崩潰。
  3. 如果你曾經(jīng)使用過(guò)AsyncTask,以后不用了;在Android 4.4以下,進(jìn)程內(nèi)也默認(rèn)有5個(gè)AsyncTask線(xiàn)程;在Android 4.4以上,默認(rèn)有CPU + 1個(gè)線(xiàn)程。
  4. Android 3.0以上的AsyncTask默認(rèn)是串行執(zhí)行任務(wù)的;如果要并行執(zhí)行需要調(diào)用低版本沒(méi)有的API,處理麻煩。

因此我們對(duì)系統(tǒng)的AsyncTask做了一些修改,在不同Android版本提供一致的行為,并且提高了使用此類(lèi)的安全性,主要改動(dòng)如下:

  1. 添加對(duì)于任務(wù)過(guò)多導(dǎo)致崩潰的異常保護(hù);在這里進(jìn)行必要的數(shù)據(jù)統(tǒng)計(jì)上報(bào)工作;如果出現(xiàn)這個(gè)問(wèn)題,說(shuō)明AsyncTask不適合這種場(chǎng)景了,需要考慮重構(gòu);
  2. 移植API 22對(duì)于Handler的處理;這樣就算在線(xiàn)程創(chuàng)建異步任務(wù),也不會(huì)有任何問(wèn)題;
  3. 提供串行執(zhí)行和并行執(zhí)行的execute方法;默認(rèn)串行執(zhí)行,如果明確知道自己在干什么,可以使用executeParallel并行執(zhí)行。
  4. doInbackgroud里面頻繁崩潰的地方加上try..catch;自己處理數(shù)據(jù)上報(bào)工作。

完整代碼見(jiàn)gist,BetterAsyncTask

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 美圖欣賞 Java、Android知識(shí)點(diǎn)匯集 Java集合類(lèi) ** Java集合相關(guān)的博客** java面試相關(guān) ...
    ElvenShi閱讀 1,893評(píng)論 0 2
  • 前段時(shí)間遇到這樣一個(gè)問(wèn)題,有人問(wèn)微信朋友圈的上傳圖片的功能怎么做才能讓用戶(hù)的等待時(shí)間較短,比如說(shuō)一下上傳9張圖片,...
    加油碼農(nóng)閱讀 1,285評(píng)論 0 2
  • 前言## 任何一個(gè)Android 開(kāi)發(fā)者對(duì)AsnycTask 都應(yīng)該不陌生;使用AsyncTask可以很方便的異步...
    IAM四十二閱讀 1,504評(píng)論 3 18
  • 在老家陽(yáng)光總是來(lái)得那么早,每天不到8點(diǎn),金黃的陽(yáng)光已經(jīng)透過(guò)窗戶(hù)鋪滿(mǎn)了整個(gè)房間。其實(shí)我不是一個(gè)矯情的人,但是每次看到...
    overall閱讀 427評(píng)論 0 0
  • “讀書(shū)讀得腦子壞掉了!”原來(lái),村里的人經(jīng)常說(shuō)這樣的話(huà),指著那個(gè)完成了高等教育,日子卻沒(méi)有風(fēng)生水起,甚至還有點(diǎn)神經(jīng)兮...
    遇見(jiàn)禾禾閱讀 3,280評(píng)論 5 8

友情鏈接更多精彩內(nèi)容