深入理解 AsyncTask

AsyncTask 是一個簡單實用的多線程異步任務工具類。

Android 開發中經常遇到需要將耗時的操作放到子線程中進行異步執行,等執行完畢之后再通知主線程更新 UI 的情況,例如異步加載網絡圖片、異步讀取文件等,如果在主線程中執行這些耗時操作,就會造成卡頓的情況,甚至是 ANR。開啟一個線程處理耗時任務很簡單,創建一個 Thread 對象,在 run() 方法中去執行耗時操作即可,但是當耗時任務執行完畢要通知主線程更新 UI 的時候就比較麻煩了,我們需要創建一個主線程的 Handler,然后通過它向主線程發送更新 UI 的消息進行 UI 更新,這一整個過程十分麻煩。

基于上訴問題, AsyncTask 就出現了,按照官方的說法,AsyncTask 是為了解決主線程和子線程之間的交互問題,它能夠在后臺線程中執行耗時操作,并在把執行結果發送到主線程,而這一系列的操作都不需要你創建任何的 Thread 或 Handler 對象。AsyncTask 適合處理耗時不是很長的任務(幾秒鐘),所以你不應該拿它來處理耗時很長的任務,至于為什么,我們會在后面解釋原因。

1 用法

假設我們有一個加載本地文件的任務,需要從網絡上加載 3 個文件,并且在加載每個文件的時候通知用戶當前正在加載什么文件,整個過程大概需要 10 秒鐘,這很明顯是一個十分耗時的任務,我們就用 AsyncTask 進行一步加載。

1.1 繼承 AsyncTask

public abstract class AsyncTask<Params, Progress, Result> { ... }

AsyncTask 本身是一個抽象類,它要求我們必須繼承它并指定三個泛型參數類型,這三個泛型參數類型依次是執行參數類型(Params)、進度值類型(Progress)和結果類型(Result):

  • 執行參數類型(Params)

    指的是在執行異步任務的時候指定的額外參數類型,例如加載文件的 URL。

  • 進度值類型(Progress)

    指的是在執行異步任務期間要更新進度情況時使用的數據類型,例如用 Integer 類型更新 ProgressBar。

  • 結果類型(Result)

    指的是異步任務執行結束后返回的結果類型,例如返回 一個 Boolean 類型的數據代表異步任務是否成功。

所以接下來我們要做的第一步就是繼承 AsyncTask 并指定參數類型:

/**
 * 加載文件的異步任務。
 */  
public class LoadFileTask extends AsyncTask<String, String, Boolean> {

}

在上面的代碼里,我們指定執行參數類型為 String 類型,因為我們在加載文件的時候需要指定三個 URL 作為參數;進度值類型為 String 類型,因為每加載一個文件的時候都要通知用戶當前加載的文件名;結果類型為 Boolean,當三個任務都加載成功的時候我們返回 true 代表文件加載成功。

指定完參數類型之后,我們還要實現幾個方法,并且在這些方法里面寫入我們的異步操作邏輯:

  • void onPreExecute()

    該方法會主線程中執行,用于在執行異步任務之前的業務邏輯,例如彈出一個 ProgressDialog。

  • Result doInBackground(Params... params)

    從方法名稱就可以知道它是在子線程中執行的,我們可以在這個方法里面執行耗時操作,例如加載文件,該方法的參數類型就是前面提到的執行參數類型,并且它是一個可變參數,也就是說我們可以一次指定多個執行參數。另外,該方法要求你返回一個你指定好的結果類型數據,該數據會在異步任務結束的時候作為 onPostExecute(Result result) 的參數。

    注意該方法是一個必須實現的抽象方法。

  • void onPostExecute(Result result)

    該方法會在主線程中執行,用于在執行異步任務完成之后的業務邏輯,例如關閉 ProgressDialog,該方法的參數類型就是前面提到的結果類型。

  • void onProgressUpdate(Progress... values)

    該方法會在主線程中執行,用于在執行異步任務期間更新進度情況,例如在加載文件的時候不斷刷新進度條,你需要通過調用 publishProgress(Progress... values) 觸發該回調,從而更新進度情況。

接下來,我們就要實現上述幾個方法,在開始加載文件之前以 Toast 形式提示用戶“開始加載文件”,在加載某一個文件的時候提示用戶“正在加載文件:xxx”,在加載完所有文件的時候根據加載文件的結果提示用戶文件是否都加載成功:

/**
 * 加載文件的異步任務。
 */  
public class LoadFilesTask extends AsyncTask<String, String, Boolean> {

    private Context mContext;
    
    public LoadFileTask(Context context) {
        mContext = context;
    }

    @Override
    public void onPreExecute() {
        // 在異步任務開始前提示用戶。
        Toast.makeText(mContext, "開始加載文件", Toast.LENGTH_SHORT).show();
    }
    
    @Override
    public Boolean doInBackground(String... params) {
        String file1 = params[0];
        String file2 = params[1];
        String file3 = params[2];
        
        boolean isSuccess = true;
        
        // 加載第一個文件,并且通知用戶當前加載的文件名稱。
        publishProgress("File1");
        isSuccess = isSuccess && loadFile(file1);
        
        // 加載第二個文件,并且通知用戶當前加載的文件名稱。
        publishProgress("File2");
        isSuccess = isSuccess && loadFile(file2);
        
        // 加載第三個文件,并且通知用戶當前加載的文件名稱。
        publishProgress("File3");
        isSuccess = isSuccess && loadFile(file3);
        
        // 返回文件加載結果。
        return isSuccess;
    }
    
    @Override
    public void onProgressUpdate(String... values) {
        // 提示用戶當前在加載的是哪個文件。
        String fileName = values[0];
        String message = "正在加載文件:" + fileName;
        Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
    }
    
    @Override
    public void onPostExecute(Boolean result) {
        // 在異步任務結束的時候根據不同的加載結果提示用戶。
        boolean isSuccess = result;
        if (isSuccess) {
            Toast.makeText(mContext, "成功加載所有文件", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(mContext, "加載文件失敗", Toast.LENGTH_SHORT).show();      
        }
    }
    
}

1.2 執行異步任務

繼承 AsyncTask 創建異步加載文件的任務之后,接下來要做的就是執行這個異步任務了,AsyncTask 提供了兩個執行異步任務的方法:

  • execute(Params...)

    使用 AsyncTask 提供的線程池執行異步任務,并且指定若干個執行參數。

    關于 AsyncTask 的線程池我們會在后面詳細說明。

  • executeOnExecutor(Executor, Params...)

    使用自己定義的線程池執行異步任務,并且指定若干個執行參數。

在這里我們就直接使用 AsyncTask 提供的線程池執行我們的文件加載任務,并且指定三個要加載的文件的路徑作為執行參數:

String file1 = "http://www.example.com/file1";
String file2 = "http://www.example.com/file2";
String file3 = "http://www.example.com/file3";
LoadFilesTask loadFilesTask = new LoadFilesTask(mContext);// 創建異步任務實例
loadFilesTask.execute(file1, file2, file3);// 執行異步任務

到此為止,我們的異步加載文件任務就開發完成了,可以說 AsyncTask 的使用方法還是很簡單的。

2 版本變化

AsyncTask 從引入到現在,經歷了幾次較大的改動,我們有必要了解這幾次的改動以免在實際開發的時候踩坑:

  • Android 1.5

    AsyncTask 作為一個異步任務工具類首次被引入 SDK,此時的 AsyncTask 只能按照串行的方式,一個接一個的執行每一個異步任務,也就是說無論你通過 AsyncTask 創建多少個異步任務,它們都會被加入隊列中按順序依次執行,如果某一個異步任務耗時很久,就會導致后面的任務一直無法被執行。

  • Android 1.6 至 3.0(不包括3.0)

    AsyncTask 開始支持多線程并發的情況,其內部有一個全局共享的線程池,該線程池的最小線程數為 5,最大線程數為 128。也就是說現在你可以創建多個 AsyncTask 實例,并且讓它們同時被執行了。

  • Android 3.0 及以上

    AsyncTask 又變成了默認情況下單線程串行方式依次執行每一個異步任務,Google 給出的理由是避免某些并發問題,具體問題估計要跟業務場景有關。如果你希望并行執行多個異步任務,可以通過 executeOnExecutor(Executor, Params...) 方法執行多線程的線程池。

3 源碼分析

現在,是時候來看看 AsyncTask 的源碼實現了,讓我們從 execute(Params…) 方法開始吧,看看里面都干了啥:

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

可以看到 execute(Params…) 方法里面就一句話,調用 executeOnExecutor(Executor, Params…) 方法執行異步任務,并且指定了一個默認的線程池叫做 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();
                }
            }
        });
        if (mActive == null) {
            scheduleNext();
        }
    }

    protected synchronized void scheduleNext() {
        if ((mActive = mTasks.poll()) != null) {
            // 最終使用的是另一個線程池來執行異步任務,這個后面會解釋。
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}

原來 sDefaultExecutor 指向的是 SerialExecutor,其內部使用了 ArrayDeque 按順序存儲要執行的異步任務,這些異步任務是按順序依次被執行的,當前一個任務執行完畢之后,就從隊列中取出下一個任務繼續執行。也許你已經看到了 THREAD_POOL_EXECUTOR 這個線程池,發現 SerialExecutor 實際上只是一個異步任務的調度中心,最終執行任務的線程池是 THREAD_POOL_EXECUTOR,我們順藤摸瓜繼續往下看:

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 BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<Runnable>(128);

// 創建 AsyncTask 線程時名稱類似:AsyncTask #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());
    }
};

/**
 * An {@link Executor} that can be used to execute tasks in parallel.
 */
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
        CORE_POOL_SIZE,
        MAXIMUM_POOL_SIZE,
        KEEP_ALIVE,
        TimeUnit.SECONDS,
        sPoolWorkQueue,
        sThreadFactory);

從代碼中可以看出 AsyncTask 內部使用的線程池就是 THREAD_POOL_EXECUTOR,我們要關注的是 CORE_POOL_SIZE、MAXIMUM_POOL_SIZE 和 sPoolWorkQueue 三個常量的定義:

  • CORE_POOL_SIZE:AsyncTask 默認的線程池最小線程數是 CPU 數加 1
  • MAXIMUM_POOL_SIZE:最大線程數是 CPU 數的兩倍再加 1
  • sPoolWorkQueue:線程池隊列中最多允許 128 個異步任務

所以,我們在使用 AsyncTask 的時候需要注意了,當我們直接使用 execute(Params…) 方法按順序執行異步任務的時候,所有的任務都是按順序依次執行的,如果某一個任務過于耗時,會導致后面任務都處于長時間等待狀態;當我們使用 executeOnExecutor(Executor, Params…) 方法執行異步任務的時候,如果直接使用 AsyncTask 的 THREAD_POOL_EXECUTOR 作為并行執行異步任務的線程池時,它最多支持 MAXIMUM_POOL_SIZE 個異步任務并行執行,并且在極端情況下你最多添加 128 個異步任務,所以我們的建議是自己創建并行執行異步任務的線程池。

討論完 AsyncTask 的線程之后,我們來看看它的執行流程,我們從 executeOnExecutor(Executor, Params…) 方法看起:

@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;

    onPreExecute();

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

    return this;
}

首先我們注意到如果你重復調用 AsyncTask 的 executeOnExecutor(Executor, Params…) 方法的話,系統會拋出異常告訴你這個 AsyncTask 已經被執行或者已經結束。當我們調用 executeOnExecutor(Executor, Params…) 方法的時候 onPreExecute() 回調就會被觸發,之后就是調用線程池安排執行我們的異步任務。你應該注意到了 mWorker 和 mFuture 兩個成員變量了吧?我們先來看下這兩個是什么東西:

private final WorkerRunnable<Params, Result> mWorker;
private final FutureTask<Result> mFuture;

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

很簡單,mWorker 實際上就是 WorkerRunnable, 它實現了 Callable 接口,并且定義了和 AsyncTask 對應的 Params 和 Result 泛型,其內部還存儲了用于執行異步任務的參數,而 mFuture 就是 FutureTask 了,用于監聽異步任務執行結果,這兩個成員變量應該說是 AsyncTask 的核心了,我們繼續看下一段代碼:

public AsyncTask() {
    mWorker = new WorkerRunnable<Params, Result>() {
        public Result call() throws Exception {
            mTaskInvoked.set(true);
            
            // 此處有坑,此處有坑,此處真的有坑,重要的事說三遍!
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            //noinspection unchecked
            Result result = doInBackground(mParams);
            Binder.flushPendingCommands();
            return postResult(result);
        }
    };

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

很明顯 mWorker 負責在其他線程中執行異步邏輯,也就是 doInBackground(Params…) 方法,并且在異步邏輯完成之后調用 postResult(Result) 方法執行 onPostExecute(Result),mFuture 在 done() 回調方法中處理一些特殊的情況,例如異步任務還沒有被執行就被取消。有一個地方需要特別注意的是執行異步邏輯的時候,調用了 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND) 方法設置當前的線程優先級為后臺級別,這是一個深坑,在某些低端的單核手機上會出現主線程由于優先級較高而一直占用 CPU 資源導致 AsyncTask 的異步任務一直無法被執行的情況。

最后,我們看看 AsyncTask 是如何處理 onPostExecute(Reuslt) 和 onProgressUpdate(Progress…) 的,源碼如下:

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

@WorkerThread
protected final void publishProgress(Progress... values) {
    if (!isCancelled()) {
        getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                new AsyncTaskResult<Progress>(this, values)).sendToTarget();
    }
}

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

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

    @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
                result.mTask.finish(result.mData[0]);
                break;
            case MESSAGE_POST_PROGRESS:
                result.mTask.onProgressUpdate(result.mData);
                break;
        }
    }
}

其實原理很簡單就是利用 Handler 將數據發送到主線程執行。

4 注意事項

最后我們總結下使用 AsyncTask 需要注意的事項:

  • Android 1.6 至 3.0(不包括 3.0)的AsyncTask 是支持并行執行異步任務的,其他版本都是按順序依次執行異步任務。
  • 如果你希望并行執行異步任務,建議你創建自己的線程池,通過 executeOnExecutor(Executor, Params…) 方法執行異步任務,盡量不用使用 AsyncTask 提供的默認線程池。
  • AsyncTask 的異步線程優先級是 Process.THREAD_PRIORITY_BACKGROUND,在某些低端的單核手機上會出現主線程由于優先級較高而一直占用 CPU 資源導致 AsyncTask 的異步任務一直無法被執行的情況。

AsyncTask 雖然好用,但是請以正確的姿勢使用,避免出現預想不到的問題。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,462評論 25 708
  • Android Handler機制系列文章整體內容如下: Android Handler機制1之ThreadAnd...
    隔壁老李頭閱讀 3,271評論 1 15
  • Android開發者:你真的會用AsyncTask嗎? 導讀.1 在Android應用開發中,我們需要時刻注意保證...
    cxm11閱讀 2,730評論 0 29
  • 從哪說起呢? 單純講多線程編程真的不知道從哪下嘴。。 不如我直接引用一個最簡單的問題,以這個作為切入點好了 在ma...
    Mr_Baymax閱讀 2,831評論 1 17
  • 當你覺得自己的生活沒有干勁、沒有激情的時候,不妨想一想如何給自己平淡的生活找一些樂趣。生活需要這些儀式感,在有些人...
    塵子223閱讀 456評論 0 0