【原始碼解析】AsyncTask的用法與規則

yilian發表於2019-11-14

引言

AsyncTask,相信大家已經很熟悉了。它的內部封裝了 ThreadHandler,這讓我們可以將一些耗時操作放到 AsyncTask,並且能將結果及時更新到UI上。 AsyncTask主要用於短時間耗時操作,長時間耗時操作不建議使用 AsyncTask。下面透過Google官方的一個例子來認識 AsyncTask的用法。

一個例子

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {  protected void onPreExecute() {
        showProgress();
  }  protected Long doInBackground(URL... urls) {      int count = urls.length;      long totalSize = 0;      for (int i = 0; i < count; i++) {
          totalSize += Downloader.downloadFile(urls[i]);
          publishProgress((int) ((i / (float) count) * 100));          // Escape early if cancel() is called
          if (isCancelled()) break;
      }      return totalSize;
  }  protected void onProgressUpdate(Integer... progress) {
      setProgressPercent(progress[0]);
  }  protected void onPostExecute(Long result) {
      showDialog("Downloaded " + result + " bytes");
  }
 }
複製程式碼

AsyncTask是一個抽象類,我們要使用時必須自定義一個類繼承於它。 AsyncTask的原型為:

public abstract class AsyncTask<Params, Progress, Result> {}
複製程式碼

它接收三個泛型引數,分別表示 引數型別、進度型別、結果型別

上述的例子中 DownloadFilesTask接收引數型別為 URL型別,使用 Integer型別表示任務進度,最終的任務結果是一個 Long型別。

注意:上面三個泛型型別不一定都得用一個明確的型別,對於沒有使用的型別,可以使用 Void型別代替。

繼承 AsyncTask至少需要重寫 doInBackground方法,同時 AsyncTask也提供了另外三個方法供我們重寫,分別是 onPreExecuteonProgressUpdateonPostExecute

  • onPreExecute方法。在任務開始執行之前執行,它執行在UI執行緒中。通常我們可以在這裡展示一個等待進度條。
  • doInBackground方法。貫穿整個耗時任務,它執行在子執行緒中。在這裡執行耗時操作。
  • onProgressUpdate方法。貫穿整個耗時任務,在 publishProgress方法被呼叫後執行,它執行在UI執行緒中。通常用於展示整個任務的一個進度。
  • onProgressUpdate方法。在任務接收後呼叫, doInBackground的返回結果會透傳給 onPostExecute的引數值,它執行在主執行緒中。通常我們從這裡獲取任務執行完成後的結果資料。

AsyncTask的規則

  • AsyncTask類必須在UI執行緒載入。(在4.1系統版本以上會自動完成)
  • AsyncTask物件必須在UI執行緒建立,也就是說 AsyncTask的構造方法必須在UI執行緒中呼叫。(經過測試 AsyncTask物件可以在子執行緒建立,只要保證 execute方法在UI執行緒執行就OK的。但是沒有人會這樣做,因為多此一舉!!!)
  • execute方法必須在UI執行緒中呼叫。這樣做是保證 onPreExecute方法執行在UI執行緒。
  • 不要主動呼叫 onPreExecutedoInBackgroundonProgressUpdateonProgressUpdate方法。
  • 單執行緒下,AsyncTask物件的任務只能執行一次,否則會報執行時錯誤。

AsyncTask執行任務的規則

AsyncTask誕生之初,任務是在一個後臺執行緒中順序執行的。從Android 1.6開始,就變成了可以在後臺執行緒中並行執行任務。然後,到了Android 3.0版本,又改成了單執行緒順序執行,以此避免併發任務產生的錯誤行為。

為了驗證上述結論,下面看一個Demo例子。

public class MainActivity extends Activity {    public static final String TAG = "MyApplication";    @Override
    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);        new MyTask("task1").execute();        new MyTask("task2").execute();        new MyTask("task3").execute();        new MyTask("task4").execute();        new MyTask("task5").execute();        new MyTask("task6").execute();
    }    private class MyTask extends AsyncTask<Void, Void, Void> {        private String taskName;
        MyTask(String taskName) {            this.taskName = taskName;
        }        @Override
        protected Void doInBackground(Void... integers) {            try {
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }            return null;
        }        @Override
        protected void onPostExecute(Void aVoid) {
            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Log.e(TAG, taskName + " finish at: " + df.format(new Date()));
        }
    }
}
複製程式碼

這個例子比較簡單,就是在 MainActivity啟動時,執行了六次 MyTask,並將任務執行後的時間節點列印出來。

image.png
image.png

手機的系統版本是Android 8.0,從上面的Log資訊可以看出, AsyncTask的確是序列執行的。由於現有測試機最低系統版本都是Android 4.1,已經很難找到Android 3.0以下的老古董機子了?,所以我們只能透過原始碼去驗證Android 1.6到Android 3.0期間, AsyncTask是否是並行執行的。

原始碼分析

Android 2.3版本

AsyncTask是否序列或者並行執行,取決於它的 execute方法。

public final AsyncTask<Params, Progress, Result> execute(Params... params) {
    ...省略 
    mWorker.mParams = params;
    sExecutor.execute(mFuture);    return this;
}
複製程式碼

execute方法中透過 sExecutor,實際為 ThreadPoolExecutor物件,它的初始化如下所示。

private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
            MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory);
複製程式碼

ThreadPoolExecutor是一個多執行緒容器,其中可以建立多個執行緒來執行多個任務。由此驗證了Android 1.6版本到Android 3.0版本直接, AsyncTask執行任務的機制的確也現在的機制不一樣,它可以讓任務並行執行。

Android 8.0版本

我們對比一下Android 8.0版本的 execute方法。

public final AsyncTask<Params, Progress, Result> execute(Params... params) {    return executeOnExecutor(sDefaultExecutor, params);
}public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
        Params... params) {
    ...省略
    mWorker.mParams = params;
    exec.execute(mFuture);    return this;
}
複製程式碼

execute方法中呼叫了 executeOnExecutor方法,並將 sDefaultExecutor作為 Executor物件傳遞進去, sDefaultExecutor的初始化如下所示。

private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;public static final Executor SERIAL_EXECUTOR = new SerialExecutor();private static class SerialExecutor implements Executor {    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    Runnable mActive;    public synchronized void execute(final Runnable r) {
        mTasks.offer(new Runnable() {            public void run() {                try {
                    r.run();
                } finally {
                    scheduleNext(); //任務執行完畢後繼續執行scheduleNext方法
                }
            }
        });        if (mActive == null) { //第一個任務會執行該方法
            scheduleNext();
        }
    }    protected synchronized void scheduleNext() {        if ((mActive = mTasks.poll()) != null) { //判斷mTask佇列中是否有下一個任務,有則取出來執行
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}
複製程式碼

可以看到,在Android 8.0版本中,建立了一個 ArrayDeque佇列,每次只從佇列中獲取一個任務執行,執行完畢後會繼續判斷佇列中是否有任務,如果有則取出來執行,直到所有任務執行完畢為止。由此可見,Android 8.0版本執行任務是序列執行的。

如果我們想改變 AsyncTask這種預設行為呢,可以修改麼?答案是肯定的。

我們可以直接呼叫 AsyncTaskexecuteOnExecutor方法,並將一個 Executor物件傳遞過去,就能變成並行的執行方法了。

對於上面的例子,可以這樣改動。

public class MainActivity extends Activity {    public static final String TAG = "MyApplication";    @Override
    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);        new MyTask("task1").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);        new MyTask("task2").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);        new MyTask("task3").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);        new MyTask("task4").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);        new MyTask("task5").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);        new MyTask("task6").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }    private class MyTask extends AsyncTask<Void, Void, Void> {        private String taskName;
        MyTask(String taskName) {            this.taskName = taskName;
        }        @Override
        protected Void doInBackground(Void... integers) {            try {
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }            return null;
        }        @Override
        protected void onPostExecute(Void aVoid) {
            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Log.e(TAG, taskName + " finish at: " + df.format(new Date()));
        }
    }
}
複製程式碼

執行後,列印出來的Log資訊如下圖所示。

image.png
image.png

注意:這裡前五個Task是同時執行的,因為AsyncTask.THREAD_POOL_EXECUTOR建立了五個核心執行緒,第六個任務需要等待空閒執行緒才能繼續執行。所以會出現第六個任務和前五個任務執行時間不一致的現象,特此說明。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69952849/viewspace-2664093/,如需轉載,請註明出處,否則將追究法律責任。

相關文章