AsyncTask異步任務 源碼解讀

之前我們介紹了Handler的一些基本用法,也解讀了Handler的源碼。通過Handler我們可以簡便的切換到主線程進行UI操作。而AsyncTask的出現使我們不用去關心線程管理和切換的一些細節,我們可以更輕鬆的去操作UI。

基本概念

AsyncTask異步任務的作用

AsyncTask,見名之意,異步任務。允許我們在後臺做一些耗時操作,然後切換到主線程更新,而且這一過程變得非常簡便。一提到異步任務,我們的第一反應就是多線程。假如我們現在需要去下載一張圖片,然後在界面上顯示,如果沒有AsyncTask,我們的異步操作可能就會這麼寫:

  • 普通寫法
   new Thread(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap=loadBitmap(url);//loadBitmap是封裝好的工具類,用來下載圖片,返回bitmap
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        mImageView.setImageBitmap(bitmap);
                    }
                });
            }
        }).start();

那麼,AsyncTask怎麼寫呢?

  • AsyncTask寫法
//AsyncTask下載並展示圖片
 new MyAsyncTask(mImageView).execute(url);

 //MyAsyncTask的實現
public class MyAsyncTask  extends AsyncTask<String,Void,Bitmap>{
    private ImageView mImageView;
    private ProgressDialog mProgressDialog;
    public MyAsyncTask(ImageView imageView){
        this.mImageView=imageView;

    }
    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        mProgressDialog=new ProgressDialog(mImageView.getContext());
        mProgressDialog.setMessage("正在下載");
        mProgressDialog.show();

    }

    @Override
    protected Bitmap doInBackground(String... params) {
          retrun loadBitmap(params[0])
      }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);
        if(bitmap!=null){
            mImageView.setImageBitmap(bitmap);
        }

    }
}

可以看出,AsyncTask的結構要清晰很多。有準備操作的方法onPreExecute,有專門執行後臺操作的方法doInBackground,也有更新UI的方法onPostExecute,甚至還有個更新進度的方法onProgressUpdate,每個方法各司其職。
你可能會想,除了結構清晰,也沒看出有其他什麼優勢啊。那麼,我們現在來想象一個場景——列表滑動。假如我們在ListView中去請求圖片,直接new線程顯然不太合適,快速滑動會開啓大量的線程,線程阻塞不說,還會消耗大量系統資源。而AsyncTask由於內部管理着一個線程池,就可以解決這些問題。

AsyncTask的工作流程

AsyncTask不僅使用起來非常簡便,此外,工作流程也非常清晰。

  1. doInBackground編寫耗時任務。
  2. 在主線程提交任務到線程池
  3. 線程池進行處理任務
  4. 處理完切換到主線程並返回結果。

AsyncTask異步任務疑問

在瞭解過AsyncTask的一些用法後,我們不難會產生一些疑問。

  • AsyncTask的線程切換是怎麼完成的?
  • AsyncTask的execute方法的可變參數怎麼用?
  • execute方法爲什麼只能調用一次?
  • execute方法爲什麼只能在主線程調用?
  • AsyncTask是串行執行還是並行執行?

讓我們帶着疑問,去源碼裏探索吧。

初識API

ThreadPoolExecutor(線程池執行者)

一看名字就知道,線程池,JAVA線程管理的一個api。通過下面的構造方法就可以建立一個線程池。

    /**
     * @param corePoolSize 核心線程數,即使空閒也一直存在,除非給核心線程也設了超時時間。
     * @param maximumPoolSize 線程池的最大線程數
     * @param keepAliveTime 除核心線程外的其他線程等待超時時間,超時就會殺死。
     * @param unit 時間單位
     * @param workQueue 工作隊列,多餘的任務就會放在這個隊列中,等待被執行。
     * @param threadFactory 線程工廠,用來創建線程
     * @param handler 當工作隊列滿後,再添加任務時的處理策略,處理策略有如下幾種,DiscardOldestPolicy:擠掉最舊的任務。DiscardPolicy:廢棄當前被添加的任務,CallerRunsPolicy:直接在添加任務的那個線程執行,AbortPolicy:直接拋異常。默認的處理策略時AbortPolicy。
     */
  public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

我們常用的Executors.newFixedThreadPool(int nThreads)等創建線程池的方法,其內部也是調用了這個構造方法。

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

Queue和Deque(隊列和雙端隊列)

Queue隊列,繼承自集合(Collection),通常遵循着先進先出原則(FIFO)(除了優先級隊列和後進先出隊列)對應的api如下。

操作 拋出異常(已滿或已空) 返回空值(已滿或已空)
添加元素 add(e) offer(e)
取出一個元素 remove() poll()
查看一個元素 element() peek()

我們一般採用offer,poll,peek方法來操作元素,以免拋出異常。

Deque雙端隊列,繼承自Queue,支持雙端移除和插入。因繼承自Queue,所以可以直接當作Queue隊列來用,此時遵守FIFO原則,當然,可以顯示指明操作隊頭還是隊尾offerFirst(e),pollFirst(),offerLast(e),pollLast()..
Deque的api對照如下

Queue方法 同等的Deque方法
add(e) addLast(e)
remove() removeFirst()
element() getFirst()
offer(e) offerLast(e)
poll() pollFirst()
peek() peekFirst()

Runable,Callable,Future,FutureTask四者的關係。

  • Runable: 一個接口,常用於執行異步任務,有一個抽象方法void run(),該方法沒有返回值。
public interface Runnable {
    public void run();
}
  • Callable: 一個接口,常用於執行異步任務,有一個抽象方法V call(),該方法含有返回值。
public interface Callable<V> {
    V call() throws Exception;
}
  • Future: Runable和Callable就像脫繮的野馬,提交給線程就沒法進行管理了。而Future則是用來管理他們的,可以取消任務,獲取結果等。
public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
  • FutureTask是Future的一個實現類。用來包裝Runable和Callable,使其可控制。常規用法如下。
   //創建一個Callable對象,在實現方法中寫耗時操作
   Callable callable= new Callable<Bitmap>() {
            @Override
            public Bitmap call() throws Exception {
                return loadBitmap(url);
            }
        };
    //使用FutureTask包裝Callable,使其可控制
    FutureTask<Bitmap> futureTask=new FutureTask<Bitmap>(callable);

    //提交到線程中 
     new Thread(futureTask).start();

     //阻塞直到獲取結果,如果不想阻塞,就重寫done方法,在裏面get()
     Bitmap bitmap= futureTask.get();

工作原理

編寫任務(重寫doInBackground方法)

這一步比較簡單,就是在doInBackground裏面寫一些耗時操作,比如網絡訪問。這裏不作演示。

提交任務到線程池(調用execute方法)

AsyncTask的使用比較簡便,一般都是以new MyAsyncTask(..).execute(url)的形式調用。那麼,execute到底做了什麼呢?在介紹之前,我們先來看一下AsyncTask的構造方法。

AsyncTask的構造方法

    public AsyncTask() {
        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);//設置任務已被調用的標識
                //...
                //省略了部分代碼
                Result result = doInBackground(mParams);//耗時操作
                //...
                //省略了部分代碼
                return postResult(result);
            }
        };

        mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                   //...
                  //省略了部分代碼
            }
        };
    }

看完上面的代碼,可能有點手足無措。不要慌張,慢慢來。
先來看看mWorker,裏面的東西是不是非常眼熟,跟Callable的寫法簡直太像了。事實上,的確繼承自Callable。而doInBackground只是Callable實現方法中的一部分。執行完過後調用了postResult來傳遞數據,postResult做了什麼暫且不做研究,後面再做介紹。WorkerRunnable的代碼如下,僅僅是定義了一個變量用於保存參數。

 private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
        Params[] mParams;
    }

再來看看mFuture,是不是也非常眼熟,包裝了一下mWorker使其可控制。
看完上面兩段源碼後,有沒有發現跟我上面舉的FutureTask的例子有點像,那麼,接下來要幹嘛相信你已經很清楚了,沒錯,就是提交到線程或者線程池中去執行這段代碼。只要被執行就能獲得結果完成使命了。
真是萬事俱備,只欠線程池啊。

調用execute方法

現在回過來看一下execute方法,看它到底做了什麼事。

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

可以看到內部經過了一次包裝,且傳入了一個默認的Executor,我們來看一下executeOnExecutor的源碼。

public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
            Params... params) {
        if (mStatus != Status.PENDING) {
            switch (mStatus) {
                case RUNNING:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task is already running.");
                case FINISHED:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task has already been executed "
                            + "(a task can be executed only once)");
            }
        }

        mStatus = Status.RUNNING;

        onPreExecute();

        mWorker.mParams = params;
        exec.execute(mFuture);

        return this;
    }

我們可以發現在這個方法中直接調用了onPreExecute,而onPreExecute是用來幹嘛的呢?做一些提交之前的操作,比如顯示一個進度框等UI操作,這也就暗示了,如果要在onPreExecute中做UI操作,則必須在主線程中調用execute方法。此外我們也能隱隱約約知道爲什麼execute只能提交一次,因爲,一旦提交過任務,就會將狀態設置爲Status.RUNNING,完成後就變爲Status.FINISHED,按照源碼邏輯可知,再也回不到Status.PENDING狀態,所以不能再提交第二次,否則會拋異常。那麼,爲什麼這麼設計呢?是因爲FutureTask的緣故,FutureTask的計算是不能被重新調用的(除非調用的是runAndReset())。
Status是一個枚舉類,如下。

    public enum Status {
        PENDING,//還未調用
        RUNNING,//調用中
        FINISHED,//完成
    }

繼續閱讀源碼,發現了這一行exec.execute(mFuture);莫非就在這裏提交給了線程池去執行?滿懷期待的找到sDefaultExecutor的源碼實現一看:

    private static class SerialExecutor implements Executor {
        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
        Runnable mActive;

        public synchronized void execute(final Runnable r) {

            //將Runable對象添加到雙端隊列裏
            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);
            }
        }
    }

What?這個線程池不是用來執行任務的?心中一萬隻草泥馬奔騰而過。沒辦法,只能繼續閱讀源碼。發現了ArrayDeque雙端隊列,一看到隊列我們的第一反應就是要排隊。可是排隊幹嘛呢?幹嘛不直接提交到線程池裏面去執行。從源碼可以看出使用了ArrayDeque雙端隊列來存放Runnable對象,你可能會疑問,不是FutureTask嗎,怎麼變成Runnable了,那是因爲FutureTask也實現了Runnable接口,莫急,接下來就是排隊的核心代碼,爲了使看起來更直觀,修改成如下形式:

new Runnable() {
    public void run() {

          try {
                r.run();
          } finally {

               //取出一個任務添加到線程池裏執行。
            if ((mActive = mTasks.poll()) != null) {
                THREAD_POOL_EXECUTOR.execute(mActive);
            }
          }
     }
 }

從上面的源碼可以看出,使用了Runnable包裝了原來的FutureTaskrun方法,巧妙的使用finally來保證任務的串行執行。
看到這裏恍然大悟SerialExecutor原來是一個用來排隊的線程池。THREAD_POOL_EXECUTOR纔是我們一直在苦苦尋找的用於執行任務的線程池。exec.execute(mFuture)先把任務保存到排隊線程池的隊列中,然後串行提交到執行任務的線程池。也就是說,每執行完一個任務後,纔會從隊列中取出下一個任務到線程池中。
看到這裏終於可以鬆一口氣了,算是看到了提交到線程池的代碼,第二步任務終於完成了。

線程池處理任務( THREAD_POOL_EXECUTOR.execute(..);

線程池的內部工作邏輯這裏就不深究了,我們只需知道目前爲止我們已經成功將任務提交到線程池中,只需等待處理,便可獲得結果。我們來看看線程池本尊是怎麼實現的。源碼如下。

//線程配置
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
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 ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
        }
    };

  //工作隊列
  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);

線程池的實現是一些很常規的代碼,沒有什麼可介紹的,唯一需要關心的問題就是線程池開多大合適。核心線程數爲N+1,最大線程數爲2N+1。

處理完畢,返回結果(回調onPostExecute

經過線程執行完畢,終於可以獲取結果呢。還記得AsyncTask的構造方法嗎?不記得也不要緊,我們再看一遍。

    public AsyncTask() {
        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);//設置任務已被調用
                //...
                //省略了部分代碼
                Result result = doInBackground(mParams);//耗時操作
                //...
                //省略了部分代碼
                return postResult(result);
            }
        };
                //...
                //省略了部分代碼
    }

由源碼可知,在處理完畢後會調用postResult,postResult的源碼如下

 private Result postResult(Result result) {
        @SuppressWarnings("unchecked")
        Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
                new AsyncTaskResult<Result>(this, result));
        message.sendToTarget();
        return result;
    }

AsyncTaskResult是一個包裝類,裏面保存了結果數據和AsyncTask對象。

   private static class AsyncTaskResult<Data> {
        final AsyncTask mTask;
        final Data[] mData;

        AsycTaskResult(AsyncTask task, Data... data) {
            mTask = task;
            mData = data;
        }
    }

此外,似乎又看到了熟悉的身影MessageHandler。看到這裏終於明白了,內部通過Handler來進行切換線程,然後更新UI。如果我沒猜錯,在Handler的handleMessage方法內,一定直接或者間接的調用了onPostExecute
爲了驗證我的猜想是否正確,順藤摸瓜找到了Handler的實現。

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

    //Handler的實現
   private static class InternalHandler extends Handler {
        public InternalHandler() {
            super(Looper.getMainLooper());
        }
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT://這個是結果
                    result.mTask.finish(result.mData[0]);
                    break;
                case MESSAGE_POST_PROGRESS://這個是進度
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }

找到我們剛剛發出的消息類型MESSAGE_POST_RESULT。發現沒有直接調用onPostExecute而是調用了AsyncTask的finish方法,繼續追溯源碼。

    private void finish(Result result) {
        if (isCancelled()) {
            onCancelled(result);
        } else {
            onPostExecute(result);
        }
        mStatus = Status.FINISHED;
    }

看完源碼才恍然大悟。原來有兩種類型啊,一種是正常執行完畢的,回調onPostExecute,還有一種是被取消了,回調onCancelled。那麼問題來了,怎麼取消一個任務?
只需調用AsyncTask的cancel(boolean)方法即可。cancel會調用FutureTask的cancel方法去中斷任務。

     //取消一個任務
    public final boolean cancel(boolean mayInterruptIfRunning) {
        mCancelled.set(true);//設置一個取消標識
        return mFuture.cancel(mayInterruptIfRunning);//取消一個任務
    }

    //獲取取消的狀態
   public final boolean isCancelled() {
        return mCancelled.get();//獲取取消標識
    }

從源碼可以看出,取消任務後是不會再走onPostExecute的,只走onCancelled方法。
看到這裏。算是把AsyncTask的源碼給看的差不多了。

最後

  • 怎麼更新進度?更新進度在哪個線程進行?
    更新進度的源碼如下。使用時,在doInBackground中進行調用。然後由Handler傳遞到主線程中,重寫onProgressUpdate接受即可。
protected final void publishProgress(Progress... values) {
        if (!isCancelled()) {
            getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                    new AsyncTaskResult<Progress>(this, values)).sendToTarget();
        }
    }


 private static class InternalHandler extends Handler {
        public InternalHandler() {
            super(Looper.getMainLooper());
        }
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
            switch (msg.what) {
                //省略了部分源碼
                case MESSAGE_POST_PROGRESS:
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }
  • AsyncTask的execute方法的可變參數怎麼用?
    從源碼來看,execute的可變參數全部傳入進Result result = doInBackground(mParams);,也就是說,自己可以結合場景巧妙使用。可以傳多個url,批量下載小文件等。

  • AsyncTask是串行執行還是並行執行?
    從以上源碼分析(Android5.0)得出,是串行執行。但是官方api文檔指出。Android1.6之前和Android3.0之後是串行執行。在這兩個版本之間採用的是並行執行。於是找了份2.3的源碼,果真如此,可以發現沒有了排隊線程池的蹤影。源碼如下:

//execute的源碼如下
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
         if (mStatus != Status.PENDING) {
              //..
              //省略了部分源碼
        }

        mStatus = Status.RUNNING;

        onPreExecute();

        mWorker.mParams = params;
      sExecutor.execute(mFuture);

        return this;
  }

//sExecutor的實現如下,可以看出直接就是一個任務線程池,沒有用於排隊的線程池。
private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
        MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory);
  • Android3.0後AsyncTask怎麼並行執行?
    其實很簡單,想個辦法跳過那個排隊線程池即可。可以調用executeOnExecutor(Executor exec, Params... params),如上所示,直接傳入一個線程池進行執行。

總結

經過解讀了AsyncTask的源碼,從側面可以看出,Handler不可動搖的地位,毋容置疑。
如果沒有看過Handler源碼的,可以閱讀這篇 Handler消息機制 源碼解讀加深理解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章