最近在項目中遇到一個比較奇葩的問題, 就是AsyncTask執行了execute方法之后, doInBackground方法很久才得到掉調用. 我們都知道doInBackground方法是在子線程中執行的, 而execute方法執行的線程(一般是在UI線程)肯定與doInBackground方法執行的線程不同.
從execute方法執行的線程切換到doInBackground方法執行的線程為什么會耗時這么久??
毫無疑問這是一個線程調度的問題. 我們不應該懷疑系統調度有問題, 那么剩下的只有我們自己調度的問題......
項目里有一個強制更新的功能, 如下圖:
點擊強制更新按鈕后會顯示進度調(正常情況), 如下圖:
點擊"強制更新"按鈕后, 按鈕被禁止點擊:
downloadApkBtn.setClickable(true);
但是按鈕沒有變灰, 按鈕上的文本也沒改成"正在下載..."
因此在某些手機上就出現這樣的情況:
點擊按鈕后, 如圖一, ProgressBar進度不動, 而且再點擊按鈕就沒有反應了(按鈕設置過selector, 是有按壓效果的), 給用戶感覺就是: 應用卡頓死掉了
初步懷疑是網絡問題, 于是把網絡連等相關步驟所耗的時間打印出來. 數據顯示, 就算網絡非常慢, 也不會花費十多秒的時間. (并且連接也有超時限制, 如果很久不能連接上, 那么就會超時, 總會得到一個結果) 有問題的手機, 點擊按鈕到進度條動起來的時間有幾十秒, 甚至幾分鐘, 有的就一直卡著不動了. 對于用戶來說, 幾秒或者十幾秒不動, 就直接退出了, 或者......
在調試中發現, 點擊按鈕后很久, doInBacground()方法中的日志輸出都沒打印出來. 因此可以得出結論: doInBackground()方法所在的線程沒有執行.
于是我把execute()方法執行到doInBackground()方法執行之間的時間也打印出來, 代碼如下:
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));
//省略 ......
}
}
多次執行啟動->點擊強制更新按鈕->退出
動作, 日志輸出如下:
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()方法所在線程不能及時執行呢?
查看AsyncTask類源碼, 發現AsyncTask里面是使用線程池來執行異步任務的, AsyncTask有兩個方法來執行移步任務:
@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內部定義的默認線程池, executeOnExecutor()使用的是開發者自定義的線程池. 我們來看看AsyncTask內部自定義的程池到底是什么?
AsyncTask內部定義的默認線程池是SerialExecutor, SerialExecutor及其相關變量的定義如下:
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這個名字可以看出, 它是一個順序執行任務的執行器. 實際上SerialExecutor并不去執行任務, 它的execute()方法會委托AsyncTask內部定義的一個線程池去執行任務, 這個默認線程池(THREAD_POOL_EXECUTOR
)的定義上面已經給出. THREAD_POOL_EXECUTOR
是一個可同時處理處理器個數+1
個任務的線程池. 然而使用execute()方法執行AsyncTask時, 并沒什么卵用, 因為SerialExecutor的execute()方法會對異步任務進行封裝, 使得任務順序執行, 跟在同一個線程中執行一樣, 從而失去并發處理特性. 如果有一個耗時任務正在執行, 那么后續任務將會一直等待前面的任務完成. 因此:
AsyncTask雖然是異步任務, 這個異步指的是與ui線程異步. 如果你想要并發執行多個任務, execute()方法不能實現, executeOnExecutor()方法可以達到目的 (必須提供正確的Executor) ----- 異步跟并發并非同一個概念.
經過上面的分析可以得知, 下載apk的異步任務之前肯定有一個耗時任務在執行, 因此導致下載apk的異步任務一直處于等待隊列之中而得不到執行. 那么如何進行驗證呢?
我們通過DDMS來查看線程狀態, 看看在下載任務之前是有正在處理的任務, 操作如下:
啟動DDMS, 在DDMS窗口左側的Devices視圖中選擇我們要調試的進程, 然后點擊頂部的Update Threads
按鈕, 這時我們可以在右側的Threads視圖中看到我們選中的進程的所有線程信息, 如圖:
查看AsyncTask類的源碼, 我們知道AsyncTask默認線程池中的工作線程的命名格式是AsyncTask #index
(index大小為 1~線程池的coreSize
), 因此我們看名稱為AsyncTask開頭的線程就行了 (不得不吐槽一下DDMS做的太爛, 居然不能按線程名排序或分類, 如果線程多了, 那就AsyncTask使用的線程中間就會夾雜其他線程, 這給觀察帶來不便), 選中某個線程后下面的狀態框中會輸出此線程的堆棧信息.
通過DDMS查看線程狀態, 發現出問題時有一個AsyncTask線程處于Native狀態, Native狀態是什么鬼? 關于線程狀態可參考 DDMS中線程狀態的說明, 此文對Native狀態的說明是:
native – executing native code – 執行了原生代碼,這個對于 帶有消息隊列的線程是正常的狀態,表示消息隊列沒有任何消息,線程在native 代碼中進行無限循環,直到消息隊列中出現新的消息,消息隊列才會返回Java 代碼處理消息。
呃, 線程在native代碼中進行無限循環......
我猜想是native層導致某個task的運行一直處于阻塞狀態, 這樣后面的task也就無法執行, 查看處于native狀態的線程的堆棧信息, 如下:
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)
在堆棧中發現了這個com.anjuke.android.newbroker.util.image.AsyncTask
, 這個類其實是從這里考過來的, 查看源碼發現跟系統的區別不大, 只是對默認線程池進行了版本處理(android 11以下的版本使用單線程來處理異步任務), 代碼如下:
/**
* 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);
根據堆棧信息可以得知, 關閉下載輸入流時, 系統阻塞了, 這是非正常關閉輸入流, 它是這樣發生的:
啟動App, 點擊
強制更新
按鈕, 這時正在下載文件了, 然后退出退出App (用戶下載了一半, 可能不想下載了), 這時就會關閉輸入流. 這時的輸入流中的數據還沒讀完, 這樣關閉入流就會阻塞 (會不會等待數據讀取完畢在關閉??). 再啟動app時, 再點擊強制更新
按鈕, 這時啟動的下載任務會一直等待前一個下載任務關閉輸入流完成. --- 退出APP, APP的進程并沒有殺掉( 除非用戶按HOME鍵手動劃掉應用或在系統設置的應用管理中強制結束進程, 用戶一般不會這么做), 因此下一次啟動的APP和上一次啟動的APP會共用AsyncTask的線程池.
我們還是用數據說話, 我在程序中打印了關閉輸入流的時間, 代碼如下:
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;
}
}
運行調試, 輸出信息如下 (反復啟動同一個app, 進程是同一個
從下面的日志中可以看出, 圓括號中的為進程ID --- 殺過一次進程):
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中多次調用了closeInputStream方法, finally塊中關閉輸入流時間為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) {
// 省略 ......
}
}
}
還有一個問題就是, 用戶啟動一次發現下載不了, 就有退出
, 那么這一動作就會加入一個下載任務, 如果用戶反復執行這一動作, 那么AsyncTask的默認線程池中就會堆積越來越多的下載任務, 而execute()方法導致任務順序執行, 這樣關閉輸入流的時間也會累積起來, 也就是啟動次數越多, 下載任務等待的時間就會越久. 由于AsyncTask是自定義的, 所以我可以更改源碼, 因此我在AsyncTask中加入了Log語句Log.e("_size_", "pending size: " + mTasks.size());
, 輸出等待執行的任務數量, 更完整的代碼如下:
@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);
}
}
}
多次執行啟動-點擊強制更新按鈕-退出
動作, 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
日志說明上面的分析是正確的, 任務的確會積累在等待隊列中.
至此問題已經分析完畢 !
解決辦法: 使用AsyncTask的executeOnExecutor()方法來執行異步任務,且為executeOnExcutor()方法提供一個合理的線程池, 或者直接用Thread來執行下載任務.
總結:
**AsyncTask的execute()方法是順序執行異步任務的 (相當于
Executors.newSingleThreadExecutor()
), 每次只執行一個任務. 如果要并發執行多個異步任務, 必須使用AsyncTask的executeOnExecutor()方法, 且必須提供正確的線程池. **建議: 盡量不要使用AsynTask的execute()方法來執行異步任務. 如果要使用execute()方法來執行異步任務, 你必須確定這個異步任務無論正常執行/或異常執行都不會耗時 (如果耗時, 會導致后續異步任務得不到及時執行), 且你并不關心它什么時候執行 (因為你無法確認在你執行異步任務之前別人是否已經使用execute()方法執行了一個耗時的異步任務).
如有錯誤, 歡迎指正!