注:本文是對Android官網(wǎng)的ANR進行的翻譯,個別不關(guān)緊要的描述就沒翻譯了,但總體意思與原文一致,原文鏈接如下:
(文章里面的“工作線程”其實就是指子線程)
首先,我們先來看下Android官方對ANR的定義:
When the UI thread of an Android app is blocked for too long, an "Application Not Responding" (ANR) error is triggered. If the app is in the foreground, the system displays a dialog to the user.
意思就是說,當一個應(yīng)用程序的UI線程被長時間阻塞時,一個ANR(Application Not Responding,應(yīng)用程序無響應(yīng))錯誤被觸發(fā)。如果這個應(yīng)用程序在前臺,系統(tǒng)會向用戶顯示一個對話框(至于對話框長什么樣,想必遇到過ANR的人都知道,我這里就不貼出來了)
我們要明確,ANR是一個非常嚴重的問題,因為APP的主線程(負責(zé)更新UI的線程)無法處理用戶輸入事件或繪制,對用戶造成極大困擾。
當以下條件中的其中一個發(fā)生時,會出現(xiàn)ANR:
當activity位于前臺時,你的程序在5秒內(nèi)沒有對輸入事件或BroadcastReceiver做出響應(yīng)(比如說按鍵、屏幕觸摸事件)
當activity不處于前臺時,你的BroadcastReceiver在相當長的時間內(nèi)沒有執(zhí)行完成
如何診斷ANR
有一些普遍的場景如下:
程序在主線程做一些涉及?I/O 的操作
程序在主線程做一些長時間的運算
主線程對其他進程有一個同步的Binder調(diào)用,但其他進程需要花
很長時間才返回
主線程在等待一個長時間的同步鎖而被阻塞,而這個長時間的操作位于另一個線程
主線程和其他線程在同一進程或通過Binder調(diào)用發(fā)生交互時,主線程出現(xiàn)死鎖。此時,主線程不僅僅是在等待一個長時間的操作完成,而且還處于死鎖狀態(tài)
以下方法可以幫你找到是以上哪個原因?qū)е翧NR:
(1)啟用?Strict mode
當你開發(fā)你的程序時,使用 StrictMode 可以幫你找到主線程內(nèi)的一些意外IO操作,你可以在 application或activity級別內(nèi)使用 StrictMode。
(2)啟用后臺ANR對話框
只有當設(shè)備的“開發(fā)者選項”里面的“顯示所有ANR”這個開關(guān)被啟用時,Android才會為那些花了長時間去處理廣播消息的APP顯示ANR對話框。所以,后臺ANR對話框并不會總是顯示,但是這個APP仍然在經(jīng)歷性能問題。
(3)Traceview
當你的程序在跑用例的時候,你可以使用 Traceview 來獲得正在運行的這個程序的 trace 信息,來確認主線程繁忙的位置。關(guān)于如何使用 Traceview,我將在下一篇博客內(nèi)介紹。
(4)拉出 traces 文件
發(fā)生ANR時,Android會保存trace信息,在較老的release版本上,設(shè)備上只有一個?/data/anr/traces.txt文件;而在新的release版本上,有多個?/data/anr/anr_* 文件。你可以使用 adb 從設(shè)備或模擬器上訪問這些traces文件:
adb root
adb shell ls /data/anr
adb pull /data/anr/<filename>
如何修復(fù)ANR問題
(1)主線程的 Slow code(運行慢的代碼)
在你的代碼里面定位主線程繁忙時間超過5秒的地方,找一些可疑的用例場景并重現(xiàn)ANR。比如下圖顯示的?Traceview 的 timeline,主線程繁忙時間超過了5秒:
?

上圖表明耗時操作的代碼發(fā)生在 onClick 函數(shù)里,舉例代碼如下:
@OverridepublicvoidonClick(View view){// 這個任務(wù)運行在主線程BubbleSort.sort(data);}
這種情況下,你需要將這段耗時代碼移到一個工作線程,Android framework提供了一些類,比如下面的示例代碼展現(xiàn)了如何使用 AsyncTask:
@OverridepublicvoidonClick(View view){newAsyncTask() {@OverrideprotectedLongdoInBackground(Integer[]... params){? ? ? ? ? ?BubbleSort.sort(params[0]);// 運行在工作線程}? ?}.execute(data);}
Traceview表明大部分代碼都運行在工作線程,如下圖,主線程能夠?qū)τ脩羰录鞒鲰憫?yīng)。
?

(2)主線程的IO
在主線程上執(zhí)行IO操作,是主線程上運行緩慢的一個普遍原因,它會造成ANR。如上一節(jié)所示,推薦將所有的IO操作都移動到工作線程。IO操作的一些例子是網(wǎng)絡(luò)和存儲,更多信息請參考執(zhí)行網(wǎng)絡(luò)操作和保存數(shù)據(jù)
(3)鎖的爭奪
在某些場景下,造成ANR的任務(wù)并不是直接在主線程執(zhí)行,如果一個工作線程獲得了某個資源的鎖,而主線程需要這個資源完成自己的任務(wù),此時可能會發(fā)生ANR。
如下圖的?Traceview timeline,大部分任務(wù)都在工作線程 AsyncTask #2 內(nèi)執(zhí)行:
?

但是,如果正在發(fā)生ANR,你應(yīng)該看下?Android Device Monitor 里面的主線程狀態(tài)。通常情況下,如果主線程準備更新UI,也響應(yīng)正常的話,主線程的狀態(tài)是 Runnable。如果主線程無法恢復(fù)執(zhí)行,然后就會處于 BLOCKED 狀態(tài),無法響應(yīng)事件。在?Android Device Monitor 上顯示的狀態(tài)是 Monitor 或者 Wait,如下表:
?

下面的 trace 表示主線程在等待一個資源而被阻塞:
...
AsyncTask #2" prio=5 tid=18 Runnable
? | group="main" sCount=0 dsCount=0 obj=0x12c333a0 self=0x94c87100
? | sysTid=25287 nice=10 cgrp=default sched=0/0 handle=0x94b80920
? | state=R schedstat=( 0 0 0 ) utm=757 stm=0 core=3 HZ=100
? | stack=0x94a7e000-0x94a80000 stackSize=1038KB
? | held mutexes= "mutator lock"(shared held)
? at com.android.developer.anrsample.BubbleSort.sort(BubbleSort.java:8)
? at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:147)
? - locked <0x083105ee> (a java.lang.Boolean)
? at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:135)
? at android.os.AsyncTask$2.call(AsyncTask.java:305)
? at java.util.concurrent.FutureTask.run(FutureTask.java:237)
? at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243)
? at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
? at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
? at java.lang.Thread.run(Thread.java:761)
...
Reviewing the trace can help you locate the code that blocks the main thread. The following code is responsible for holding the lock that blocks the main thread in the previous trace:
分析 trace可以幫助你定位阻塞主線程的代碼,以下代碼就是持有了阻塞主線程的鎖:
@Override
public void onClick(View v) {
? // 工作線程獲得了 lockedResource 的鎖
? new LockTask().execute(data);
? synchronized (lockedResource) {
? ? ? // 主線程在這里需要 lockedResource,但是它必須等待 LockTask 使用完成
? }
}
public class LockTask extends AsyncTask<Integer[], Integer, Long> {
? @Override
? protected Long doInBackground(Integer[]... params) {
? ? ? synchronized (lockedResource) {
? ? ? ? ? // 這是一個長時間運行的操作,使得鎖持續(xù)了一段時間
? ? ? ? ? BubbleSort.sort(params[0]);
? ? ? }
? }
}
另一個例子是主線程正在等待另一個工作線程的結(jié)果,如下代碼所示。
public void onClick(View v) {
? WaitTask waitTask = new WaitTask();
? synchronized (waitTask) {
? ? ? try {
? ? ? ? ? waitTask.execute(data);
? ? ? ? ? // 等待工作線程的通知
? ? ? ? ? waitTask.wait();
? ? ? } catch (InterruptedException e) {}
? }
}
class WaitTask extends AsyncTask<Integer[], Integer, Long> {
? @Override
? protected Long doInBackground(Integer[]... params) {
? ? ? synchronized (this) {
? ? ? ? ? BubbleSort.sort(params[0]);
? ? ? ? ? // 結(jié)束,通知主線程
? ? ? ? ? notify();
? ? ? }
? }
}
還有一些其他場景可以阻塞主線程,包括使用 Lock、Semaphore、資源池(比如數(shù)據(jù)庫連接池)的線程或其他 mutex 機制。你應(yīng)該評估你的程序中對一般資源所持有的鎖,但是如果你想避免ANR,你應(yīng)該注意下主線程所需要的那些資源所持有的鎖。確保鎖被持有最少時間,評估下這個程序首先是否需要鎖,如果你使用鎖并基于工作線程的處理來決定何時更新UI,使用像onProgressUpdate()andonPostExecute()這種機制來實現(xiàn)主線程和工作線程之間的通信。
死鎖
當一個線程所需要的資源被另一個線程持有時,它進入了等待狀態(tài),而另一個線程也在等待被第一個線程持有的資源,就會發(fā)生死鎖。如果主線程處于這種情況,很有可能發(fā)生ANR。死鎖是計算機科學(xué)里面研究的比較好的現(xiàn)象,而且你可以使用一些死鎖預(yù)防算法來避免死鎖,更多細節(jié)請參考Wikipedia上的DeadlockandDeadlock prevention algorithms。
執(zhí)行緩慢的廣播接收器
應(yīng)用程序可以對廣播消息作出響應(yīng),比如啟用或禁用飛行模式、網(wǎng)絡(luò)連接狀態(tài)的改變,都可以由廣播接收器實現(xiàn)。當程序長時間處理廣播消息時,會發(fā)生ANR。
ANR發(fā)生在下列情況:
BroadcastReceiver在相當長的時間內(nèi)沒有執(zhí)行完它的 onReceive() 方法
BroadcastReceiver調(diào)用了 goAsync() 方法,然后在 PendingResult 對象上調(diào)用 finish() 失敗了
在 BroadcastReceiver 的 onReceive() 方法里面只應(yīng)該執(zhí)行短時間的操作,如果你的程序需要處理一個更加復(fù)雜的廣播消息,你應(yīng)該將你的任務(wù)交給 IntentService 執(zhí)行。你可以使用像 Traceview 這樣的工具來確認你的 Receiver 是否在主線程內(nèi)執(zhí)行長時間的操作,比如,下圖的 timeline 顯示了 broadcast receiver在主線程處理了一個消息將近 100秒:
?

這個行為可以被 BroadcastReceiver 的 onReceive() 方法內(nèi)執(zhí)行長時間操作而引起,比如下面的示例代碼:
@OverridepublicvoidonReceive(Context context, Intent intent){// 長時間的操作BubbleSort.sort(data);}
像這種情況,推薦將長時間操作的代碼移動到 IntentService 去實現(xiàn),因為它使用了一個工作線程去執(zhí)行任務(wù)。下面的代碼展示了如何使用 IntentService 去處理一個長時間操作:
@OverridepublicvoidonReceive(Context context, Intent intent){// 現(xiàn)在這個任務(wù)運行在工作線程Intent intentService =newIntent(context, MyIntentService.class);? ? context.startService(intentService);}publicclassMyIntentServiceextendsIntentService{@OverrideprotectedvoidonHandleIntent(@Nullable Intent intent){? ? ? BubbleSort.sort(data);? }}
As a result of using theIntentService, the long-running operation is executed on a worker thread instead of the main thread. Figure 7 shows the work deferred to the worker thread in the Traceview timeline.
使用 IntentService 的結(jié)果是,這個長時間的操作不是執(zhí)行在主線程,而是執(zhí)行在一個工作線程,如下圖所示:
?

你的廣播接收器可以使用 goAsync() 方法向系統(tǒng)發(fā)出信號,告訴它需要更多時間去處理消息。然而,你還應(yīng)該在 PendingResult 對象上調(diào)用 finish() 方法。下面這個例子展示了如何調(diào)用 finish() 方法讓系統(tǒng)去回收廣播消息和避免ANR:
finalPendingResult pendingResult = goAsync();newAsyncTask() {@OverrideprotectedLongdoInBackground(Integer[]... params){// 長時間的操作BubbleSort.sort(params[0]);? ? ? pendingResult.finish();? }}.execute(data);
然而,如果這個廣播處于后臺,將代碼移到另一個線程并使用 goAsync() 并不會修復(fù)ANR,ANR超時仍然會生效。
更多關(guān)于ANR的信息,請參考Keeping your app responsive。更多關(guān)于線程的信息,請參考Threading performance。