最近在項(xiàng)目中遇到一個(gè)比較奇葩的問題, 就是AsyncTask執(zhí)行了execute方法之后, doInBackground方法很久才得到掉調(diào)用. 我們都知道doInBackground方法是在子線程中執(zhí)行的, 而execute方法執(zhí)行的線程(一般是在UI線程)肯定與doInBackground方法執(zhí)行的線程不同.
從execute方法執(zhí)行的線程切換到doInBackground方法執(zhí)行的線程為什么會耗時(shí)這么久??
毫無疑問這是一個(gè)線程調(diào)度的問題. 我們不應(yīng)該懷疑系統(tǒng)調(diào)度有問題, 那么剩下的只有我們自己調(diào)度的問題......
項(xiàng)目里有一個(gè)強(qiáng)制更新的功能, 如下圖:
點(diǎn)擊強(qiáng)制更新按鈕后會顯示進(jìn)度調(diào)(正常情況), 如下圖:
點(diǎn)擊"強(qiáng)制更新"按鈕后, 按鈕被禁止點(diǎn)擊:
downloadApkBtn.setClickable(true);
但是按鈕沒有變灰, 按鈕上的文本也沒改成"正在下載..."
因此在某些手機(jī)上就出現(xiàn)這樣的情況:
點(diǎn)擊按鈕后, 如圖一, ProgressBar進(jìn)度不動, 而且再點(diǎn)擊按鈕就沒有反應(yīng)了(按鈕設(shè)置過selector, 是有按壓效果的), 給用戶感覺就是: 應(yīng)用卡頓死掉了
初步懷疑是網(wǎng)絡(luò)問題, 于是把網(wǎng)絡(luò)連等相關(guān)步驟所耗的時(shí)間打印出來. 數(shù)據(jù)顯示, 就算網(wǎng)絡(luò)非常慢, 也不會花費(fèi)十多秒的時(shí)間. (并且連接也有超時(shí)限制, 如果很久不能連接上, 那么就會超時(shí), 總會得到一個(gè)結(jié)果) 有問題的手機(jī), 點(diǎn)擊按鈕到進(jìn)度條動起來的時(shí)間有幾十秒, 甚至幾分鐘, 有的就一直卡著不動了. 對于用戶來說, 幾秒或者十幾秒不動, 就直接退出了, 或者......
在調(diào)試中發(fā)現(xiàn), 點(diǎn)擊按鈕后很久, doInBacground()方法中的日志輸出都沒打印出來. 因此可以得出結(jié)論: doInBackground()方法所在的線程沒有執(zhí)行.
于是我把execute()方法執(zhí)行到doInBackground()方法執(zhí)行之間的時(shí)間也打印出來, 代碼如下:
private volatile long start_time;
private void downloadApk() {
start_time = System.currentTimeMillis();
new DownloadApkTask().execute();
}
class DownloadApkTask extends AsyncTask<Void, Integer, Boolean> {
protected Boolean doInBackground(Void... params) {
Log.e("_ajk_", "schedule time cast: " + ((System.currentTimeMillis() - start_time) / 1000.0));
//省略 ......
}
}
多次執(zhí)行啟動->點(diǎn)擊強(qiáng)制更新按鈕->退出
動作, 日志輸出如下:
macbook-stonedeMacBook-Pro:~ stone$ adb logcat *:E | grep schedule
E/_ajk_ (25685): schedule time cost: 0.014
E/_ajk_ (25685): schedule time cost: 21.618
E/_ajk_ (25685): schedule time cost: 27.859
E/_ajk_ (25685): schedule time cost: 0.004
E/_ajk_ (25685): schedule time cost: 38.368
E/_ajk_ (25685): schedule time cost: 10.722
E/_ajk_ (25685): schedule time cost: 1.66
E/_ajk_ (25685): schedule time cost: 45.632
E/_ajk_ (25685): schedule time cost: 25.374
E/_ajk_ (25685): schedule time cost: 23.161
E/_ajk_ (25685): schedule time cost: 28.733
E/_ajk_ (25685): schedule time cost: 44.852
E/_ajk_ (25685): schedule time cost: 62.171
E/_ajk_ (25685): schedule time cost: 93.201
E/_ajk_ (25685): schedule time cost: 95.367
E/_ajk_ (25685): schedule time cost: 108.974
E/_ajk_ (25685): schedule time cost: 122.609
E/_ajk_ (25685): schedule time cost: 129.158
E/_ajk_ (25685): schedule time cost: 134.632
為什么doInBackground()方法所在線程不能及時(shí)執(zhí)行呢?
查看AsyncTask類源碼, 發(fā)現(xiàn)AsyncTask里面是使用線程池來執(zhí)行異步任務(wù)的, AsyncTask有兩個(gè)方法來執(zhí)行移步任務(wù):
@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, 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;
}
execute()方法使用的是AsyncTask內(nèi)部定義的默認(rèn)線程池, executeOnExecutor()使用的是開發(fā)者自定義的線程池. 我們來看看AsyncTask內(nèi)部自定義的程池到底是什么?
AsyncTask內(nèi)部定義的默認(rèn)線程池是SerialExecutor, SerialExecutor及其相關(guān)變量的定義如下:
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);
/**
* 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);
/**
* An {@link Executor} that executes tasks one at a time in serial
* order. This serialization is global to a particular process.
*/
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
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);
}
}
}
從SerialExecutor這個(gè)名字可以看出, 它是一個(gè)順序執(zhí)行任務(wù)的執(zhí)行器. 實(shí)際上SerialExecutor并不去執(zhí)行任務(wù), 它的execute()方法會委托AsyncTask內(nèi)部定義的一個(gè)線程池去執(zhí)行任務(wù), 這個(gè)默認(rèn)線程池(THREAD_POOL_EXECUTOR
)的定義上面已經(jīng)給出. THREAD_POOL_EXECUTOR
是一個(gè)可同時(shí)處理處理器個(gè)數(shù)+1
個(gè)任務(wù)的線程池. 然而使用execute()方法執(zhí)行AsyncTask時(shí), 并沒什么卵用, 因?yàn)镾erialExecutor的execute()方法會對異步任務(wù)進(jìn)行封裝, 使得任務(wù)順序執(zhí)行, 跟在同一個(gè)線程中執(zhí)行一樣, 從而失去并發(fā)處理特性. 如果有一個(gè)耗時(shí)任務(wù)正在執(zhí)行, 那么后續(xù)任務(wù)將會一直等待前面的任務(wù)完成. 因此:
AsyncTask雖然是異步任務(wù), 這個(gè)異步指的是與ui線程異步. 如果你想要并發(fā)執(zhí)行多個(gè)任務(wù), execute()方法不能實(shí)現(xiàn), executeOnExecutor()方法可以達(dá)到目的 (必須提供正確的Executor) ----- 異步跟并發(fā)并非同一個(gè)概念.
經(jīng)過上面的分析可以得知, 下載apk的異步任務(wù)之前肯定有一個(gè)耗時(shí)任務(wù)在執(zhí)行, 因此導(dǎo)致下載apk的異步任務(wù)一直處于等待隊(duì)列之中而得不到執(zhí)行. 那么如何進(jìn)行驗(yàn)證呢?
我們通過DDMS來查看線程狀態(tài), 看看在下載任務(wù)之前是有正在處理的任務(wù), 操作如下:
啟動DDMS, 在DDMS窗口左側(cè)的Devices視圖中選擇我們要調(diào)試的進(jìn)程, 然后點(diǎn)擊頂部的Update Threads
按鈕, 這時(shí)我們可以在右側(cè)的Threads視圖中看到我們選中的進(jìn)程的所有線程信息, 如圖:
查看AsyncTask類的源碼, 我們知道AsyncTask默認(rèn)線程池中的工作線程的命名格式是AsyncTask #index
(index大小為 1~線程池的coreSize
), 因此我們看名稱為AsyncTask開頭的線程就行了 (不得不吐槽一下DDMS做的太爛, 居然不能按線程名排序或分類, 如果線程多了, 那就AsyncTask使用的線程中間就會夾雜其他線程, 這給觀察帶來不便), 選中某個(gè)線程后下面的狀態(tài)框中會輸出此線程的堆棧信息.
通過DDMS查看線程狀態(tài), 發(fā)現(xiàn)出問題時(shí)有一個(gè)AsyncTask線程處于Native狀態(tài), Native狀態(tài)是什么鬼? 關(guān)于線程狀態(tài)可參考 DDMS中線程狀態(tài)的說明, 此文對Native狀態(tài)的說明是:
native – executing native code – 執(zhí)行了原生代碼,這個(gè)對于 帶有消息隊(duì)列的線程是正常的狀態(tài),表示消息隊(duì)列沒有任何消息,線程在native 代碼中進(jìn)行無限循環(huán),直到消息隊(duì)列中出現(xiàn)新的消息,消息隊(duì)列才會返回Java 代碼處理消息。
呃, 線程在native代碼中進(jìn)行無限循環(huán)......
我猜想是native層導(dǎo)致某個(gè)task的運(yùn)行一直處于阻塞狀態(tài), 這樣后面的task也就無法執(zhí)行, 查看處于native狀態(tài)的線程的堆棧信息, 如下:
at libcore.io.Posix.recvfromBytes(Native Method)
at libcore.io.Posix.recvfrom(Posix.java:141)
at libcore.io.BlockGuardOs.recvfrom(BlockGuardOs.java:164)
at libcore.io.IoBridge.recvfrom(IoBridge.java:550)
at java.net.PlainSocketImpl.read(PlainSocketImpl.java:506)
at java.net.PlainSocketImpl.access$000(PlainSocketImpl.java:46)
at java.net.PlainSocketImpl$PlainSocketInputStream.read(PlainSocketImpl.java:240)
at org.apache.http.impl.io.AbstractSessionInputBuffer.fillBuffer(AbstractSessionInputBuffer.java:103)
at org.apache.http.impl.io.AbstractSessionInputBuffer.read(AbstractSessionInputBuffer.java:134)
at org.apache.http.impl.io.ContentLengthInputStream.read(ContentLengthInputStream.java:174)
at org.apache.http.impl.io.ContentLengthInputStream.read(ContentLengthInputStream.java:188)
at org.apache.http.impl.io.ContentLengthInputStream.close(ContentLengthInputStream.java:121)
at org.apache.http.conn.BasicManagedEntity.streamClosed(BasicManagedEntity.java:179)
at org.apache.http.conn.EofSensorInputStream.checkClose(EofSensorInputStream.java:266)
at org.apache.http.conn.EofSensorInputStream.close(EofSensorInputStream.java:213)
at com.anjuke.android.newbroker.activity.AutoUpdateActivity$DownloadApkTask.closeInputStream(AutoUpdateActivity.java:345)
at com.anjuke.android.newbroker.activity.AutoUpdateActivity$DownloadApkTask.doInBackground(AutoUpdateActivity.java:289)
at com.anjuke.android.newbroker.activity.AutoUpdateActivity$DownloadApkTask.doInBackground(AutoUpdateActivity.java:202)
at com.anjuke.android.newbroker.util.image.AsyncTask$2.call(AsyncTask.java:337)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at com.anjuke.android.newbroker.util.image.AsyncTask$SerialExecutor$1.run(AsyncTask.java:279)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
at java.lang.Thread.run(Thread.java:848)
在堆棧中發(fā)現(xiàn)了這個(gè)com.anjuke.android.newbroker.util.image.AsyncTask
, 這個(gè)類其實(shí)是從這里考過來的, 查看源碼發(fā)現(xiàn)跟系統(tǒng)的區(qū)別不大, 只是對默認(rèn)線程池進(jìn)行了版本處理(android 11以下的版本使用單線程來處理異步任務(wù)), 代碼如下:
/**
* An {@link Executor} that executes tasks one at a time in serial order.
* This serialization is global to a particular process.
*/
public static final Executor SERIAL_EXECUTOR = Utils.hasHoneycomb() ? new SerialExecutor() :
Executors.newSingleThreadExecutor(sThreadFactory);
根據(jù)堆棧信息可以得知, 關(guān)閉下載輸入流時(shí), 系統(tǒng)阻塞了, 這是非正常關(guān)閉輸入流, 它是這樣發(fā)生的:
啟動App, 點(diǎn)擊
強(qiáng)制更新
按鈕, 這時(shí)正在下載文件了, 然后退出退出App (用戶下載了一半, 可能不想下載了), 這時(shí)就會關(guān)閉輸入流. 這時(shí)的輸入流中的數(shù)據(jù)還沒讀完, 這樣關(guān)閉入流就會阻塞 (會不會等待數(shù)據(jù)讀取完畢在關(guān)閉??). 再啟動app時(shí), 再點(diǎn)擊強(qiáng)制更新
按鈕, 這時(shí)啟動的下載任務(wù)會一直等待前一個(gè)下載任務(wù)關(guān)閉輸入流完成. --- 退出APP, APP的進(jìn)程并沒有殺掉( 除非用戶按HOME鍵手動劃掉應(yīng)用或在系統(tǒng)設(shè)置的應(yīng)用管理中強(qiáng)制結(jié)束進(jìn)程, 用戶一般不會這么做), 因此下一次啟動的APP和上一次啟動的APP會共用AsyncTask的線程池.
我們還是用數(shù)據(jù)說話, 我在程序中打印了關(guān)閉輸入流的時(shí)間, 代碼如下:
private void closeInputStream(InputStream inputStream) throws IOException {
if (inputStream != null) {
long startTime = System.currentTimeMillis();
inputStream.close();
Log.e("_close_", "close inputstream cost: " +((System.currentTimeMillis() - startTime) / 1000.0) + "s");
inputStream = null;
}
}
運(yùn)行調(diào)試, 輸出信息如下 (反復(fù)啟動同一個(gè)app, 進(jìn)程是同一個(gè)
從下面的日志中可以看出, 圓括號中的為進(jìn)程ID --- 殺過一次進(jìn)程):
macbook-stonedeMacBook-Pro:~ stone$ adb logcat *:E | grep _close_
E/_close_ (20394): close inputstream cost: 76.06s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (20394): close inputstream cost: 30.284s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (20394): close inputstream cost: 31.251s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (20394): close inputstream cost: 28.734s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (20394): close inputstream cost: 27.762s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (20394): close inputstream cost: 35.533s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (20394): close inputstream cost: 25.023s
E/_close_ (20394): close inputstream cost: 0.001s
E/_close_ (20394): close inputstream cost: 17.888s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (20394): close inputstream cost: 17.883s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (20394): close inputstream cost: 23.678s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (21037): close inputstream cost: 134.213s
E/_close_ (21037): close inputstream cost: 0.0s
E/_close_ (21037): close inputstream cost: 56.464s
E/_close_ (21037): close inputstream cost: 0.0s
E/_close_ (21037): close inputstream cost: 40.104s
E/_close_ (21037): close inputstream cost: 0.0s
E/_close_ (21037): close inputstream cost: 34.067s
E/_close_ (21037): close inputstream cost: 0.0s
由于doInBackground中多次調(diào)用了closeInputStream方法, finally塊中關(guān)閉輸入流時(shí)間為0. doInBackground方法的基本邏輯如下:
protected Boolean doInBackground(Void... params) {
InputStream inputStream = null;
// 省略 ......
try {
// 省略 ......
if (inputStream != null) {
while (!isCancelDownload && !isFinishing()) {
//download apk file .......
}
}
closeInputStream(inputStream);
return true;
} catch (Exception e) {
// 省略 ......
return false;
} finally {
try {
closeInputStream(inputStream);
} catch (IOException e) {
// 省略 ......
}
}
}
還有一個(gè)問題就是, 用戶啟動一次發(fā)現(xiàn)下載不了, 就有退出
, 那么這一動作就會加入一個(gè)下載任務(wù), 如果用戶反復(fù)執(zhí)行這一動作, 那么AsyncTask的默認(rèn)線程池中就會堆積越來越多的下載任務(wù), 而execute()方法導(dǎo)致任務(wù)順序執(zhí)行, 這樣關(guān)閉輸入流的時(shí)間也會累積起來, 也就是啟動次數(shù)越多, 下載任務(wù)等待的時(shí)間就會越久. 由于AsyncTask是自定義的, 所以我可以更改源碼, 因此我在AsyncTask中加入了Log語句Log.e("_size_", "pending size: " + mTasks.size());
, 輸出等待執(zhí)行的任務(wù)數(shù)量, 更完整的代碼如下:
@TargetApi(11)
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();
}
Log.e("_size_", "pending size: " + mTasks.size());
}
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
多次執(zhí)行啟動-點(diǎn)擊強(qiáng)制更新按鈕-退出
動作, log日志如下:
macbook-stonedeMacBook-Pro:~ stone$ adb logcat *:E | grep _size_
E/_size_ (21890): pending size: 0
E/_size_ (21890): pending size: 1
E/_size_ (21890): pending size: 2
E/_size_ (21890): pending size: 2
E/_size_ (21890): pending size: 3
E/_size_ (21890): pending size: 4
E/_size_ (21890): pending size: 5
E/_size_ (21890): pending size: 6
E/_size_ (21890): pending size: 7
日志說明上面的分析是正確的, 任務(wù)的確會積累在等待隊(duì)列中.
至此問題已經(jīng)分析完畢 !
解決辦法: 使用AsyncTask的executeOnExecutor()方法來執(zhí)行異步任務(wù),且為executeOnExcutor()方法提供一個(gè)合理的線程池, 或者直接用Thread來執(zhí)行下載任務(wù).
總結(jié):
**AsyncTask的execute()方法是順序執(zhí)行異步任務(wù)的 (相當(dāng)于
Executors.newSingleThreadExecutor()
), 每次只執(zhí)行一個(gè)任務(wù). 如果要并發(fā)執(zhí)行多個(gè)異步任務(wù), 必須使用AsyncTask的executeOnExecutor()方法, 且必須提供正確的線程池. **建議: 盡量不要使用AsynTask的execute()方法來執(zhí)行異步任務(wù). 如果要使用execute()方法來執(zhí)行異步任務(wù), 你必須確定這個(gè)異步任務(wù)無論正常執(zhí)行/或異常執(zhí)行都不會耗時(shí) (如果耗時(shí), 會導(dǎo)致后續(xù)異步任務(wù)得不到及時(shí)執(zhí)行), 且你并不關(guān)心它什么時(shí)候執(zhí)行 (因?yàn)槟銦o法確認(rèn)在你執(zhí)行異步任務(wù)之前別人是否已經(jīng)使用execute()方法執(zhí)行了一個(gè)耗時(shí)的異步任務(wù)).
如有錯(cuò)誤, 歡迎指正!