理解 AsyncTask 原理

我愛宋慧喬發表於2019-02-28

本人只是 Android小菜一個,寫技術文件只是為了總結自己在最近學習到的知識,從來不敢為人師,如果裡面有些不正確的地方請大家盡情指出,謝謝!

1. 概述

之前講解了能夠在後臺工作執行緒中執行耗時任務的IntentService框架,在這裡我們繼續學習Android提供的另外一個非同步執行任務的框架AsyncTask,它和IntentService既有相似點也有不同點,其相似之處在於都能在新的執行緒中執行耗時任務防止阻塞主執行緒,不同之處在於AsyncTask能夠追蹤任務的執行過程和結果並在主執行緒中顯示出來。

我們先用一個簡單的示例演示下該如何使用AsyncTask,再通過原始碼分析其內部實現原理。

2. AsyncTask 的使用方式

在開始使用前,先來看下Android SDK中對AsyncTask的介紹,請宣告如下:

/**
 * <p>AsyncTask enables proper and easy use of the UI thread. This class allows you
 * to perform background operations and publish results on the UI thread without
 * having to manipulate threads and/or handlers.</p>
 *
 * <p>AsyncTask is designed to be a helper class around {@link Thread} and {@link Handler}
 * and does not constitute a generic threading framework. AsyncTasks should ideally be
 * used for short operations (a few seconds at the most.) If you need to keep threads
 * running for long periods of time, it is highly recommended you use the various APIs
 * provided by the <code>java.util.concurrent</code> package such as {@link Executor},
 * {@link ThreadPoolExecutor} and {@link FutureTask}.</p>
 */
public abstract class AsyncTask<Params, Progress, Result> { ... }
複製程式碼

AsyncTask是一個基於ThreadHandler設計的幫助類,其允許在後臺執行緒執行耗時任務並把處理進度和結果釋出到主執行緒,不過一般適用於執行時間相對較短的任務,一般執行時間不要超過幾秒。

2.1 使用示例

先來看一段示例程式碼:

private class DownloadAsyncTask extends AsyncTask<String, Integer, Long> {
    @Override
    public void onPreExecute() {
        mProgress.setVisibility(View.VISIBLE);
        mProgress.setMax(100);
        mProgress.setProgress(0);
    }

    @Override
    public Long doInBackground(String... uris) {
        int count = uris.length;
        long size = 0;
        for (int i = 1; i <= count; i ++) {
            try {
                // 休眠5秒模擬下載過程
                Thread.sleep(5 * 1000);
                // 假設每個下載檔案的大小為(序號*100)
                size += i * 100;
                // 釋出進度更新
                publishProgress( (100* i )/count);
            } catch (InterruptedException ie) {
                ie.printStackTrace();
            }
        }
        return size;
    }

    @Override
    public void onProgressUpdate(Integer... progress) {
        mProgress.setProgress(progress[0]);
    }

    @Override
    public void onPostExecute(Long result) {
        mText.setText(String.valueOf(result));
    }
}
複製程式碼

這段程式碼主要是模擬了檔案下載過程,在下載過程中實時更新進度,並在下載完成後在介面顯示下載檔案的總大小。

通過這段程式碼可以看到要使用AsyncTask實行非同步任務是非常容易的,只需要做兩件事:

  • 確定在整個處理過程中需要的引數型別,包括Params,ProgressResult,分別對應著輸入引數、進度引數和結果引數。
  • 實現必要的回撥方法,其中必須是實現的是doInBackground,耗時任務正是在這裡進行處理的,可以想象doInBackground一定是在子執行緒裡進行的;其他可選實現方法包括onPreExecute,onProgressUpdateonPostExecute,這些在示例中都參與了UI的更新,所以一定是在主執行緒中進行的。

2.2 引數介紹

首先看下AsyncTask中對引數的宣告:

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

可以發現AsyncTask中使用的都是泛型引數,在使用過程中要根據需求選擇合適的引數型別,在示例中使用的引數型別分別是String,IntegerLong,如果某一個引數是不需要的,可以用Void來表示,下面通過一個表格來對每個引數進行說明:

引數宣告 含義 作用 產生處/呼叫處 注意事項
Params 輸入引數 任務開始執行時客戶端傳送開始引數 execute()中傳送,在doInBackground()中呼叫。 可變參型別
Progress 過程引數 任務後臺執行過程中服務端釋出的當前執行進度 在doInBackground()中產生並通過publishProgess()傳送,在onProgressUpdate()呼叫。 可變參型別
Result 結果引數 任務執行完成後服務端傳送的執行結果 在doInBackground()中產生並在onPostExecute()中呼叫。

引數型別不能是基本資料型別,要使用對應的封裝型別,例如示例的ProgressResult引數使用的IntegerLong而不是intlong

2.3 回撥介面

AsyncTask中有幾個重要的回撥介面,下面分別介紹:

  • onPreExecute(): 在主執行緒中執行,主要是在後臺執行緒開始執行任務之前進行某些UI的初始化,例如進度條的顯示,可選擇實現,其宣告如下:
/**
 * Runs on the UI thread before {@link #doInBackground}.
 */
@MainThread
protected void onPreExecute() { }
複製程式碼
  • doInBackground: 在後臺執行緒中執行,主要是接收客戶端傳送過來的引數,在後臺執行耗時任務併發布執行進度和執行結果,例如檔案下載任務,是在使用過程中必須實現的介面,其宣告如下:
/**
 * Override this method to perform a computation on a background thread. The
 * specified parameters are the parameters passed to {@link #execute}
 * by the caller of this task.
 *
 * This method can call {@link #publishProgress} to publish updates
 * on the UI thread.
 *
 * @param params The parameters of the task.
 *
 * @return A result, defined by the subclass of this task.
 *
 */
@WorkerThread
protected abstract Result doInBackground(Params... params);
複製程式碼
  • publishProgress: 在後臺執行緒中執行,主要是釋出任務的當前執行進度,以方便在主執行緒中顯示,不需要重新實現直接呼叫,其宣告如下:
/**
 * This method can be invoked from {@link #doInBackground} to
 * publish updates on the UI thread while the background computation is
 * still running. Each call to this method will trigger the execution of
 * {@link #onProgressUpdate} on the UI thread.
 *
 * {@link #onProgressUpdate} will not be called if the task has been
 * canceled.
 *
 * @param values The progress values to update the UI with.
 */
@WorkerThread
protected final void publishProgress(Progress... values) {
    if (!isCancelled()) {
        getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                new AsyncTaskResult<Progress>(this, values)).sendToTarget();
    }
}
複製程式碼
  • onProgressUpdate: 在主執行緒中執行,主要是更新當前的執行進度,例如更新進度條進度,可選擇實現,其宣告如下:
/**
 * Runs on the UI thread after {@link #publishProgress} is invoked.
 * The specified values are the values passed to {@link #publishProgress}.
 *
 * @param values The values indicating progress.
 */
@SuppressWarnings({"UnusedDeclaration"})
@MainThread
protected void onProgressUpdate(Progress... values) { }
複製程式碼
  • onPostExecute: 在主執行緒中執行,主要是接收doInBackground返回的執行結果並在主執行緒中顯示,例如顯示下載檔案大小,可選擇實現,其宣告如下:
/**
 * <p>Runs on the UI thread after {@link #doInBackground}. The
 * specified result is the value returned by {@link #doInBackground}.</p>
 * 
 * <p>This method won't be invoked if the task was cancelled.</p>
 *
 * @param result The result of the operation computed by {@link #doInBackground}.
 */
@SuppressWarnings({"UnusedDeclaration"})
@MainThread
protected void onPostExecute(Result result) { }
複製程式碼

通過一個表格來總結下這些重要的回撥方法:

回撥方法 執行執行緒 作用 執行順序 是否需要重新實現
onPreExecute 主執行緒 在開始執行後臺任務前進行初始化 首先開始執行 可選
doInBackground 後臺執行緒 執行後臺耗時任務完成後返回結果 onPreExecute 執行完成後執行 必須實現
publishProgress 後臺執行緒 在執行任務過程中釋出執行進度 在 doInBackground 中執行 無須實現,直接呼叫。
onProgressUpdate 主執行緒 接收進度並在主執行緒處理 在 publishProgress 之後執行 可選
onPostExecute 主執行緒 接收執行結果並在主執行緒處理 在 doInBackground 執行完成後執行 可選

3. AsyncTask 的原理

前面已經大致分析了AsyncTask的使用方法以及重要的回撥方法,現在來看下其內部是如何實現的,主要關注兩方面的資訊:如何進行執行緒的切換和如何組織呼叫上述的回撥方法。

客戶端是通過AsyncTask.execute()來開啟非同步任務的,我們就以這個為切入點來分析,首先看下execute()做了什麼:

/**
 * Executes the task with the specified parameters. The task returns
 * itself (this) so that the caller can keep a reference to it.
 * 
 * <p>Note: this function schedules the task on a queue for a single background
 * thread or pool of threads depending on the platform version.  When first
 * introduced, AsyncTasks were executed serially on a single background thread.
 * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
 * to a pool of threads allowing multiple tasks to operate in parallel. Starting
 * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being
 * executed on a single thread to avoid common application errors caused
 * by parallel execution.  If you truly want parallel execution, you can use
 * the {@link #executeOnExecutor} version of this method
 * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings
 * on its use.
 *
 * <p>This method must be invoked on the UI thread.
 */
@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
    return executeOnExecutor(sDefaultExecutor, params);
}
複製程式碼

execute()內部直接呼叫executeOnExecutor()來開啟非同步任務,這點我們稍後分析,其宣告中有段話需要特別留意:這個任務是放在一個單獨的後臺執行緒中順序執行還是放線上程池中並行執行,和具體平臺有關。在AsyncTask最初被引進的時候是在一個單獨的後臺執行緒中順序執行的,後續幾次改變,目前預設是順序執行的。如果確實想並行執行的話,就需要直接呼叫executeOnExecutor()並且傳入合適的Executor。 現在重新回到對execute()的分析,由於其內部呼叫的是executeOnExecutor(sDefaultExecutor, params),那就來看下這個函式在做什麼?

/**
 * <p>This method must be invoked on the UI thread.
 *
 * @param exec The executor to use.  {@link #THREAD_POOL_EXECUTOR} is available as a
 *              convenient process-wide thread pool for tasks that are loosely coupled.
 * @param params The parameters of the task.
 *
 * @return This instance of AsyncTask.
 *
 * @throws IllegalStateException If {@link #getStatus()} returns either
 *         {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
 *
 */
@MainThread
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;
    // 回撥方法中首先被呼叫的方法,由於"execute()"是在主執行緒中執行的,
    // 目前為止也沒有進行執行緒的切換,所以"onPreExecute"也是在主執行緒中執行的。
    onPreExecute();

    // 通過幾次“封裝”,把引數傳入"Executor"內部,等待執行。
    mWorker.mParams = params;
    exec.execute(mFuture);

    return this;
}
複製程式碼

整段程式碼的前半部分比較好理解,就是進行狀態判斷和更新,然後呼叫onPreExecute介面,接下來的兩行程式碼咋一看實在讓人費解。不要急不要慌,一步步來分析,首先要搞清楚mWorker是什麼,來看下相關程式碼:

private final WorkerRunnable<Params, Result> mWorker;
    
mWorker = new WorkerRunnable<Params, Result>() {
    public Result call() throws Exception {
        mTaskInvoked.set(true);
        Result result = null;
        try {
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            //noinspection unchecked
            result = doInBackground(mParams);
            Binder.flushPendingCommands();
        } catch (Throwable tr) {
            mCancelled.set(true);
            throw tr;
        } finally {
            postResult(result);
        }
        return result;
    }
};

private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
    Params[] mParams;
}
複製程式碼

可以看到mWorkerWorkerRunnable的物件,而WorkerRunnable又擴充了Callable介面,看下Callable介面是什麼:

/**
 * A task that returns a result and may throw an exception.
 * Implementors define a single method with no arguments called
 * {@code call}.
 *
 * <p>The {@code Callable} interface is similar to {@link
 * java.lang.Runnable}, in that both are designed for classes whose
 * instances are potentially executed by another thread.  A
 * {@code Runnable}, however, does not return a result and cannot
 * throw a checked exception.
 *
 * <p>The {@link Executors} class contains utility methods to
 * convert from other common forms to {@code Callable} classes.
 *
 * @see Executor
 * @since 1.5
 * @author Doug Lea
 * @param <V> the result type of method {@code call}
 */
@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}
複製程式碼

在這段說明裡,把CallableRunnable進行了對比,它們都是用來在其他執行緒裡執行的,不同之處在於Callable中的call()方法能夠返回結果和檢查異常,而Runnable中的run沒有這個功能,所以可以簡單地把Callable當做Runnable來看待,只是被執行的方法是call()而且可以返回結果。由於WorkerRunnable繼承了Callable,又新增了引數,就可以把mWorker當做一個既可以接受引數有可以在執行後返回結果的Runnable物件。 現在來看mFuture是什麼,相關程式碼如下:

private final FutureTask<Result> mFuture;

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);
        }
    }
};
複製程式碼

可以看到mFutureFutureTask物件並且是利用mWorker作為引數進行構造的,看下FutureTask是什麼:

/**
 * A cancellable asynchronous computation.  This class provides a base
 * implementation of {@link Future}, with methods to start and cancel
 * a computation, query to see if the computation is complete, and
 * retrieve the result of the computation.  The result can only be
 * retrieved when the computation has completed; the {@code get}
 * methods will block if the computation has not yet completed.  Once
 * the computation has completed, the computation cannot be restarted
 * or cancelled (unless the computation is invoked using
 * {@link #runAndReset}).
 *
 * <p>A {@code FutureTask} can be used to wrap a {@link Callable} or
 * {@link Runnable} object.  Because {@code FutureTask} implements
 * {@code Runnable}, a {@code FutureTask} can be submitted to an
 * {@link Executor} for execution.
 */
public class FutureTask<V> implements RunnableFuture<V> { 
    // 核心方法,其他方法省略。
    public void run() {
    if (state != NEW ||
        !U.compareAndSwapObject(this, RUNNER, null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    // 執行封裝的 Callable.call() 方法並得到計算結果
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }
}
複製程式碼

FutureTask是用來進行非同步計算的,這個計算可以開始、取消、查詢等等,而且可以用來封裝Callable,其中核心的方法就是run(),通過上面的程式碼邏輯可以看到在run()中最核心的就是呼叫所封裝的Callable.call()來進行計算並得到結果。

mFuture所封裝的正是mWorker,所以其最終結果就是呼叫mWorkder.call()方法,現在在返回來看看之前不理解的那幾行程式碼:

mWorker.mParams = params;
exec.execute(mFuture);
複製程式碼

這兩行程式碼就是把mFuture交給執行緒池來執行,完成了執行緒的切換,也就是說mFuture的執行是在後臺執行緒中進行的,mFuture最終會呼叫mWorker.call()方法,再回過頭來看看mWorker.call()做了什麼:

mWorker = new WorkerRunnable<Params, Result>() {
    public Result call() throws Exception {
        // 設定了任務呼叫的狀態為“被呼叫”
        mTaskInvoked.set(true);
        Result result = null;
        try {
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            //noinspection unchecked
            // 執行耗時任務,這時候已經在新的執行緒裡了。
            result = doInBackground(mParams);
            Binder.flushPendingCommands();
        } catch (Throwable tr) {
            mCancelled.set(true);
            throw tr;
        } finally {
            // 分發執行結果
            postResult(result);
        }
        return result;
    }
}; 
複製程式碼

最終會在新的執行緒裡執行doInBackground(),執行之後就通過postResult()分發執行結果:

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

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

    AsyncTaskResult(AsyncTask task, Data... data) {
        mTask = task;
        mData = data;
    }
}
複製程式碼

postResult()會通過getHandler()來構建Message物件,最終把結果包裝成AsyncTaskResult進行分發,那getHandler()返回的是哪個Handler呢?

private Handler getHandler() {
    return mHandler;
}

public AsyncTask(@Nullable Looper callbackLooper) {
    mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
        ? getMainHandler()
        : new Handler(callbackLooper);
...
}

// 呼叫這個建構函式來建立AsyncTask例項
public AsyncTask() {
    this((Looper) null);
}
複製程式碼

從這段程式碼可以看到getHandler()返回的是mHandler物件,而mHandler是在AsyncTask裡進行建立的,由於在建立AsyncTask例項是並沒有傳入callbackLooper,最終的效果就是mHandler是通過getMainHandler()例項化的,得到的是主執行緒的Handler,其程式碼如下:

private static Handler getMainHandler() {
    synchronized (AsyncTask.class) {
        if (sHandler == null) {
            sHandler = new InternalHandler(Looper.getMainLooper());
        }
        return sHandler;
    }
}
複製程式碼

兜兜轉轉,doInBackground()的執行結果會交給InternalHandler來處理:

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

    @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
    @Override
    public void handleMessage(Message msg) {
        // 獲取執行結果
        AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
        switch (msg.what) {
            case MESSAGE_POST_RESULT:
                // There is only one result
                // 處理 doInBackground()的執行結果
                result.mTask.finish(result.mData[0]);
                break;
            case MESSAGE_POST_PROGRESS:
                // 處理 publishProgress()釋出的執行進度
                result.mTask.onProgressUpdate(result.mData);
                break;
        }
    }
}
複製程式碼

InternalHandler接收到doInBackground()的執行結果後,會呼叫result.mTask.finish()來繼續處理結果,實際呼叫的就是AsyncTask.finish(),來看看其程式碼:

private void finish(Result result) {
    if (isCancelled()) {
        onCancelled(result);
    } else {
        // 正常結束時呼叫 onPostExecute()處理執行結果
        onPostExecute(result);
    }
    // 更新狀態為“完成”
    mStatus = Status.FINISHED;
}
複製程式碼

在這裡終於看到onPostExecute()得到呼叫了,由於mHandler是用主執行緒HandleronPostExecute()也就是在主執行緒中執行的了。 到這裡,終於理清了execute()->onPreExecute()->doInBackground()->onPostExecute()的整個呼叫過程了,也明白了在整個過程中的執行緒切換。

細心的讀者會發現onProgressUpdate()在哪裡得到呼叫的並沒有提及,這個問題就留給大家去完成吧,相信通過前面層層解析,這個問題會很容易得到解決!

4. 總結

本文先通過一個簡單的模擬下載任務的示例展示了AsyncTask的使用方法並分析其引數型別和重要回撥方法,最後通過層層遞進地分析原始碼,明白了AsyncTask的內部原理和在執行任務過程中是如何進行執行緒切換的。當然,還有很多細節在文中並沒有提及,但這並不會影響對其原理的理解,“忽略無關細節”也是我們平時學習原始碼過程中經常採用的方法。

相關文章