之前我們介紹了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不僅使用起來非常簡便,此外,工作流程也非常清晰。
- 在
doInBackground
編寫耗時任務。 - 在主線程提交任務到線程池
- 線程池進行處理任務
- 處理完切換到主線程並返回結果。
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
包裝了原來的FutureTask
的run
方法,巧妙的使用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;
}
}
此外,似乎又看到了熟悉的身影Message
和Handler
。看到這裏終於明白了,內部通過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消息機制 源碼解讀加深理解。