AsyncTask源碼分析

基礎(chǔ)知識(shí)點(diǎn)

1.線程池Executor
2.Future
3.Callable
4.中斷線程
5.Handler
6.枚舉
7.泛型
8.單例模式
9.可變參數(shù)

AsyncTask設(shè)計(jì)思想

1.回調(diào)函數(shù)
2.如何有序調(diào)度任務(wù)
3.串行或并行執(zhí)行任務(wù)
4.中斷任務(wù)
5.線程調(diào)度

回調(diào)函數(shù)

AsyncTask,是可異步執(zhí)行任務(wù)的工具類。開發(fā)者只需要至少重寫一個(gè)回調(diào)函數(shù)doInBackground(Params... params);就能完成線程調(diào)度

我們看看它的使用方法:
我們寫一個(gè)抽象類AsyncTask的具體子類MyAsyncTask,并覆寫其四個(gè)函數(shù)。

private class MyAsynckTask extends AsyncTask<Long, String, String> {
    /**
     * 任務(wù)被執(zhí)行前,會(huì)被調(diào)用,UI線程做一些提示操作,如顯示進(jìn)度條
     */
    @Override protected void onPreExecute() {
      super.onPreExecute();
      Log.d(TAG, "onPreExecute: " + Thread.currentThread().getName());
    }  

    /**
     * 在后臺(tái)子線程執(zhí)行任務(wù),返回類型String
     * @param params  模擬計(jì)算時(shí)間
     * @return
     */    

    @Override protected String doInBackground(Long... params) {
      try {
        TimeUnit.SECONDS.sleep(params[0]);//模擬后臺(tái)耗時(shí)計(jì)算,參數(shù)大小作為計(jì)算時(shí)間
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      Log.d(TAG, "doInBackground: " + Thread.currentThread().getName());
      return Thread.currentThread().getName();
    }


    /**
     * 任務(wù)執(zhí)行完畢后,在主線程回調(diào)這個(gè)函數(shù)。
     * @param s  后臺(tái)任務(wù)返回的結(jié)果
     */
    @Override protected void onPostExecute(String s) {
      super.onPostExecute(s);
      Log.d(TAG, "onPostExecute: " + s + " ---" + Thread.currentThread().getName());
    }

    /**
     * 任務(wù)中途被取消會(huì)回調(diào)它
     */
    @Override protected void onCancelled() {
      super.onCancelled();
      Log.d(TAG, "onCancelled: ");
    }
  }

我們主要看doInBackground(Parms...parm)函數(shù),它是什么時(shí)候被回調(diào)的呢?在后臺(tái)FutrueTask類執(zhí)行call()函數(shù)時(shí)回調(diào)。 這里WorkerRunable是一個(gè)實(shí)現(xiàn)了Callable接口的實(shí)現(xiàn)類,它可以執(zhí)行任務(wù)后能攜帶一個(gè)返回值Result,并拓展了可以持有一個(gè)Parms參數(shù)。我們看看它的回調(diào)時(shí)刻:

  public AsyncTask() {
    mWorker = new WorkerRunnable<Params, Result>() {
        public Result call() throws Exception {
             /省略一些代碼
            Result result = null;
            result = doInBackground(mParams);
            ````省略一些代碼
            return result;
        }
  };

我們?cè)赨I線程使用它即可:

   new MyAsynckTask().execute(100l);
   new MyAsynckTask().execute(5l);
   new MyAsynckTask().execute(5l);
   new MyAsynckTask().execute(5l);

輸出結(jié)果:

04-03 22:44:47.713 4256-4256/com.fulaan.asynctaskdemo D/MainActivity: onPreExecute: main
04-03 22:44:47.713 4256-4256/com.fulaan.asynctaskdemo D/MainActivity: onPreExecute: main
04-03 22:44:47.713 4256-4256/com.fulaan.asynctaskdemo D/MainActivity: onPreExecute: main
04-03 22:44:47.714 4256-4256/com.fulaan.asynctaskdemo D/MainActivity: onPreExecute: main
04-03 22:44:52.746 4256-4295/com.fulaan.asynctaskdemo D/MainActivity: doInBackground: AsyncTask #1
04-03 22:44:52.747 4256-4256/com.fulaan.asynctaskdemo D/MainActivity: onPostExecute: AsyncTask #1 ---main
04-03 22:44:57.788 4256-4376/com.fulaan.asynctaskdemo D/MainActivity: doInBackground: AsyncTask #2
04-03 22:44:57.788 4256-4256/com.fulaan.asynctaskdemo D/MainActivity: onPostExecute: AsyncTask #2 ---main
04-03 22:45:02.832 4256-4450/com.fulaan.asynctaskdemo D/MainActivity: doInBackground: AsyncTask #3
04-03 22:45:02.833 4256-4256/com.fulaan.asynctaskdemo D/MainActivity: onPostExecute: AsyncTask #3 ---main
04-03 22:45:07.875 4256-4450/com.fulaan.asynctaskdemo D/MainActivity: doInBackground: AsyncTask #3
04-03 22:45:07.875 4256-4256/com.fulaan.asynctaskdemo D/MainActivity: onPostExecute: AsyncTask #3 ---main

所以,我們?cè)诨惱锩鎸懸恍┖瘮?shù)鉤子,并調(diào)用,然后然使用者在具體實(shí)現(xiàn)類里面去實(shí)現(xiàn)。

如何結(jié)合線程池串行調(diào)度任務(wù)

從上面的輸出結(jié)果來看,AsyncTask是默認(rèn)串行執(zhí)行的(不考慮版本問題)。它用了兩個(gè)調(diào)度器Executor實(shí)現(xiàn)。

  • SERIAL_EXECUTOR,用隊(duì)列保存等待被被執(zhí)行的任務(wù)
    *THREAD_POOL_EXECUTOR。異步執(zhí)行任務(wù)

SerialExecutor串行調(diào)度。我們把任務(wù)裝入一個(gè)隊(duì)列數(shù)據(jù)結(jié)構(gòu),一個(gè)個(gè)排隊(duì)執(zhí)行, 代碼:

 private static class SerialExecutor implements Executor {
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();//一個(gè)環(huán)形隊(duì)列用來保存提交過來的任務(wù)。
    Runnable mActive;//表示可立即執(zhí)行的任務(wù)。

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

我們直接看SerialExecutor在同步方法execute(r)中的實(shí)現(xiàn),它有兩步:
第一步: 保存任務(wù)到隊(duì)列。新建一個(gè)Runable包裹提交進(jìn)來的任務(wù),在try作用域里面先執(zhí)行r.run(),必須等任務(wù)執(zhí)行完后,在finally作用域中會(huì)執(zhí)行 scheduleNext(),來調(diào)度下一個(gè)任務(wù)。因此任務(wù)是串行執(zhí)行的。最后我們會(huì)把這個(gè)新建的Runnable對(duì)象放入隊(duì)列mTasks中。

第二步:查看是否有可立即執(zhí)行的任務(wù)mActive。 如果有,則立即放入THREAD_POOL_EXECUTOR執(zhí)行。

我們?cè)倏纯磗cheduleNext()函數(shù),就是調(diào)用mTasks.poll())從隊(duì)列得到下一個(gè)任務(wù),執(zhí)行。
文字描述也許會(huì)迷糊,不如看下圖[采用OmniGraffle軟件制作]:

串行調(diào)度圖.png

至此,為什么要兩個(gè)調(diào)度器呢?我認(rèn)為是為了遵守單一職責(zé)的設(shè)計(jì)原則。一個(gè)調(diào)度器只負(fù)責(zé)一件事,前面SerialExecutor我們已經(jīng)分析。那THREAD_POOL_EXECUTOR線程池調(diào)度器則是根據(jù)CPU數(shù)構(gòu)造線程Thread緩存池,節(jié)約資源,提高AsyncTask執(zhí)行性能。

3.為什么要串行執(zhí)行任務(wù)?不串行可以嗎?

為什么要串行執(zhí)行,我認(rèn)為是為了保證數(shù)據(jù)一致性。正如官方文檔說的:

tasks are executed on a single thread to avoid common application errors caused by parallel execution.</p>

  • If you truly want parallel execution, you can invoke
    executeOnExecutor(java.util.concurrent.Executor, Object[])} with
  • THREAD_POOL_EXECUTOR.

意思是說,串行模式能夠避免一些并發(fā)異常。如果你真的需要并發(fā)執(zhí)行,這可以調(diào)用executeOnExecutor(),傳入自定義一個(gè)Executor,這樣就不會(huì)去調(diào)用SerialExecutor了。而我認(rèn)為AsyncTask只適合執(zhí)行一些小任務(wù),因?yàn)樵诖袌?zhí)行時(shí),如果某個(gè)任務(wù)執(zhí)行時(shí)間過長,或者中途阻塞,占有CPU太久,則會(huì)導(dǎo)致后面排隊(duì)的任務(wù)等待過久。

4.如何取消任務(wù)

我們知道為了能得到后臺(tái)線程運(yùn)行后的結(jié)果,我們的任務(wù)實(shí)現(xiàn)了的Callable接口,它和Runnable區(qū)別是有一個(gè)返回值。現(xiàn)在我們想取消任務(wù),我們可以實(shí)現(xiàn)Future接口,它除了能通過get()得到返回值,還有cancle()方法用來取消任務(wù),done()回調(diào)函數(shù)監(jiān)聽是否任務(wù)結(jié)束

在AsyncTask中,我們用FutureTask這個(gè)系統(tǒng)SDK為我們實(shí)現(xiàn)了Ruanble和Future的實(shí)現(xiàn)類,它可以通過一個(gè)Callable類型的對(duì)象參數(shù)構(gòu)造出來,這里使用我們的是上文中的WorkRunnable作為參數(shù)。
代碼如下:

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

那么如果我們想取消任務(wù),現(xiàn)在是不是直接調(diào)用mFuture.cancel(true)強(qiáng)制中斷線程就好了?答案是否定的。因?yàn)閙Future.cancel(true)方法內(nèi)部實(shí)現(xiàn)機(jī)制其實(shí)是:

   //.....省略一些代碼
   if (mayInterruptIfRunning) {
                try {
                    Thread t = runner;
                    if (t != null)
                        t.interrupt();
                } finally { // final state
                    U.putOrderedInt(this, STATE, INTERRUPTED);
                }
            }
   //.....省略一些代碼

意思是如果任務(wù)正在運(yùn)行,則interrupt()中斷線程。而在這里,如果任務(wù)正好還在排隊(duì)尚未執(zhí)行,而我們想取消未執(zhí)行的任務(wù),那么直接調(diào)用mFuture.cancel(true)就無效!因此,我們還需要考慮未執(zhí)行的任務(wù)如何中斷。

令人驚喜的是,在AsyncTask中還提供了一個(gè)對(duì)象:

 private final AtomicBoolean mCancelled = new AtomicBoolean();

它是一個(gè)原子類,通過CAS機(jī)制保證了mCancelled可見性,它內(nèi)部保存了當(dāng)前任務(wù)是否被取消的信號(hào)。

那么正確完整的取消任務(wù)方案是分兩個(gè)步驟:
第一步:調(diào)用AsyncTask中的cancel方法:

    public final boolean cancel(boolean mayInterruptIfRunning) {
        mCancelled.set(true);//用來取消未執(zhí)行的任務(wù)
        return mFuture.cancel(mayInterruptIfRunning);//用來取消正在執(zhí)行的任務(wù)可能會(huì)拋出異常
    }

第二步:在doInBackground回調(diào)函數(shù)中循環(huán)去判斷mCancelled中的值確認(rèn)將要執(zhí)行的任務(wù)是否要求取消:

 @Override protected String doInBackground(Long... params) {
      while (!isCancelled()){
        //todo something...
      }
      return null;
    }

至此,我們成功實(shí)現(xiàn)了取消任務(wù)功能。

線程調(diào)度

在安卓系統(tǒng)中,我們要從子線程更新UI主線程,當(dāng)然是利用handler+looper+messageQueue機(jī)制,在AsyncTask中不例外也是如此。但是除此之外,我們可以學(xué)習(xí)如何通過單例模式構(gòu)造一個(gè)handler,如何強(qiáng)制構(gòu)造一個(gè)主線程執(zhí)行的handler。
1.單例模式:

 private static Handler getHandler() {
        synchronized (AsyncTask.class) {
            if (sHandler == null) {
                sHandler = new InternalHandler();
            }
            return sHandler;
        }
    }

看看源碼中的這個(gè)單例模式,比較簡陋,也不是什么double-check :)

2.強(qiáng)制在主線程執(zhí)行handler

 private static class InternalHandler extends Handler {
        public InternalHandler() {
            super(Looper.getMainLooper());
        }
}

如上在父類構(gòu)造函數(shù) [super(Looper.getMainLooper())] 初始化Looper。

執(zhí)行流程圖

總調(diào)度圖.png

總結(jié)

AsyncTask作為Activity內(nèi)部類使用的時(shí)候還會(huì)因?yàn)槌钟衋citivity對(duì)象引用,在后臺(tái)導(dǎo)致內(nèi)存泄漏問題。推薦閱讀其他執(zhí)行異步任務(wù)類框架, 如Job、Rxjava等。

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

推薦閱讀更多精彩內(nèi)容

  • 使用AsyncTask的一般步驟是: 定義一個(gè)類繼承自AsyncTask,實(shí)現(xiàn)抽象方法 new 一個(gè)AsyncTa...
    yk_looper閱讀 394評(píng)論 0 2
  • 轉(zhuǎn)載請(qǐng)注明出處:http://www.lxweimin.com/p/531657db36f4 上一篇主要說了下Asy...
    朋永閱讀 283評(píng)論 0 0
  • 在Android開發(fā)道路上,有一個(gè)類你是無論如何都無法繞過去的。那就是AsyncTask,因?yàn)槭褂玫淖銐蚝唵危诿?..
    人失格閱讀 579評(píng)論 1 3
  • 元亨,利貞;勿用有攸往,利建侯。 學(xué)習(xí)了建構(gòu)主義之后,接觸到任何好知識(shí),都想著如何轉(zhuǎn)變成可以讓大家都能掌握的課程,...
    承謙閱讀 431評(píng)論 0 1
  • 緣起 可能只是偶然,可能是想了解下王興這個(gè)人。 英語representation講公司或創(chuàng)始人文化時(shí),有人講了王興...
    im天行閱讀 566評(píng)論 0 1