一、ANR簡(jiǎn)介
ANR全稱:Application Not Responding,也就是應(yīng)用程序無響應(yīng)。
Android系統(tǒng)中,ActivityManagerService(簡(jiǎn)稱AMS)和WindowManagerService(簡(jiǎn)稱WMS)會(huì)檢測(cè)App的響應(yīng)時(shí)間,如果App在特定時(shí)間無法相應(yīng)屏幕觸摸或鍵盤輸入時(shí)間,或者特定事件沒有處理完畢,就會(huì)出現(xiàn)ANR。
以下四個(gè)條件都可以造成ANR發(fā)生:
InputDispatching Timeout:5秒內(nèi)無法響應(yīng)屏幕觸摸事件或鍵盤輸入事件
BroadcastQueue Timeout :在執(zhí)行前臺(tái)廣播(BroadcastReceiver)的onReceive()函數(shù)時(shí)10秒沒有處理完成,后臺(tái)為60秒。
Service Timeout :前臺(tái)服務(wù)20秒內(nèi),后臺(tái)服務(wù)在200秒內(nèi)沒有執(zhí)行完畢。
ContentProvider Timeout :ContentProvider的publish在10s內(nèi)沒進(jìn)行完。
1.1 避免
盡量避免在主線程(UI線程)中作耗時(shí)操作。
那么耗時(shí)操作就放在子線程中。
關(guān)于多線程可以參考:Android多線程:理解和簡(jiǎn)單使用總結(jié)
二、ANR分析辦法
2.1 ANR重現(xiàn)
這里使用的是號(hào)稱Google親兒子的Google Pixel xl(Android 8.0系統(tǒng))做的測(cè)試,生成一個(gè)按鈕跳轉(zhuǎn)到ANRTestActivity,在后者的onCreate()中主線程休眠20秒:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_anr_test);
// 這是Android提供線程休眠函數(shù),與Thread.sleep()最大的區(qū)別是
// 該使用該函數(shù)不會(huì)拋出InterruptedException異常。
SystemClock.sleep(20 * 1000);
}
二、ANR出現(xiàn)場(chǎng)景
發(fā)生ANR時(shí)會(huì)調(diào)用AppNotRespondingDialog.show()方法彈出對(duì)話框提示用戶,該對(duì)話框的依次調(diào)用關(guān)系如下圖所示:

AppErrors.appNotResponding(),該方法是最終彈出ANR對(duì)話框的唯一入口,調(diào)用該方法的場(chǎng)景才會(huì)有ANR提示,也可以認(rèn)為在主線程中執(zhí)行無論再耗時(shí)的任務(wù),只要最終不調(diào)用該方法,都不會(huì)有ANR提示,也不會(huì)有ANR相關(guān)日志及報(bào)告;通過調(diào)用關(guān)系可以看出哪些場(chǎng)景會(huì)導(dǎo)致ANR,有以下四種場(chǎng)景:
- InputDispatching Timeout:5秒內(nèi)無法響應(yīng)屏幕觸摸事件或鍵盤輸入事件。
-
BroadcastQueue Timeout :在執(zhí)行前臺(tái)廣播(BroadcastReceiver)的
onReceive()函數(shù)時(shí)10秒沒有處理完成,后臺(tái)為60秒。 - Service Timeout :前臺(tái)服務(wù)20秒內(nèi),后臺(tái)服務(wù)在200秒內(nèi)沒有執(zhí)行完畢。
- ContentProvider Timeout :ContentProvider的publish在10s內(nèi)沒進(jìn)行完。
三、出現(xiàn)ANR原因
- 主線程慢代碼
- 主線程IO
- 鎖競(jìng)爭(zhēng)
- 死鎖
四、 如何避免ANR
1.UI線程盡量只做跟UI相關(guān)的工作;
2.耗時(shí)的工作(比如數(shù)據(jù)庫(kù)操作,I/O,連接網(wǎng)絡(luò)或者別的有可能阻礙UI線程的操作)把它放入單獨(dú)的線程處理;
3.盡量用Handler來處理UI thread和別的thread之間的交互;
4.實(shí)在繞不開主線程,可以嘗試通過Handler延遲加載;
5.廣播中如果有耗時(shí)操作,建議放在IntentService中去執(zhí)行,或者通過goAsync() + HandlerThread分發(fā)執(zhí)行。
五、分析ANR的重點(diǎn)
1.cpu占用率方面:
可以通過分析各進(jìn)程的CPU時(shí)間占用率,來判斷是否為某些進(jìn)程長(zhǎng)期占用CPU導(dǎo)致該進(jìn)程無法獲取到足夠的CPU處理時(shí)間,而導(dǎo)致ANR重點(diǎn)關(guān)注下CPU的負(fù)載,各個(gè)進(jìn)程總的CPU時(shí)間占用率,用戶CPU時(shí)間占用率,核心態(tài)CPU時(shí)間占用率,以及iowait CPU時(shí)間占用率。
2.內(nèi)存方面
主要看當(dāng)前應(yīng)用native和dalvik層內(nèi)存使用情況,結(jié)合系統(tǒng)給每個(gè)應(yīng)用分配的最大內(nèi)存來分析。
ANR日志分析
當(dāng)app出現(xiàn)ANR時(shí)會(huì)在data/anr/目錄下生成traces.txt日志文件。每次發(fā)生ANR時(shí)都會(huì)刪除舊的traces文件,重新創(chuàng)建新文件。也就是說Android只保留最后一次發(fā)生ANR時(shí)的信息。
首先,我們可以使用adb命令導(dǎo)出traces文件:
adb pull /data/anr/traces.txt d:\
友情提示:traces.txt默認(rèn)會(huì)被導(dǎo)出到Android SDK的\platform-tools目錄。
開發(fā)中最方便的是在log里面就可以看到ANR的相關(guān)信息,以下面的日志為例,我們可以從Android studio logcat很明顯的看出ANR發(fā)生的原因,用戶的輸入超時(shí)了,問題線程的PID:879。
同時(shí)我們還可以通俗易懂的看出來 CPU平均負(fù)載,CPU的使用情況:
4.67 ,3.32 ,1.49 分別表示 發(fā)生ANR 前一分鐘,五分鐘,十五分鐘 CPU的平均負(fù)載 Load: 4.67 / 3.32 / 1.49 CPU usage from 6021ms to 79ms ago。

接下來還是回到進(jìn)一步分析traces.txt文件上來,看文件里面的內(nèi)容:
----- pid 879 at 2019-01-02 08:05:04 -----Cmd line: com.sandiyu.lcd JNI: CheckJNI is off; workarounds are off; pins=2; globals=273 DALVIK THREADS:(mutexes: tll=0 tsl=0 tscl=0 ghl=0) "main" prio=5 tid=1 WAIT | group="main" sCount=1 dsCount=0 obj=0x4159cd68 self=0x414d6510 | sysTid=879 nice=0 sched=0/0 cgrp=apps handle=1074020692 | state=S schedstat=( 0 0 0 ) utm=602 stm=168 core=1 at java.lang.Object.wait(Native Method) - waiting on <0x4159ce38> (a java.lang.VMThread) held by tid=1 (main) at java.lang.Thread.parkFor(Thread.java:1205) at sun.misc.Unsafe.park(Unsafe.java:325) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:157) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2017) at java.util.concurrent.LinkedBlockingQueue.put(LinkedBlockingQueue.java:318) at com.sandiyu.lcd.utils.DeviceCommandSender$CommandSendThread.send(DeviceCommandSender.java:156) at com.sandiyu.lcd.utils.DeviceCommandSender.displayNull(DeviceCommandSender.java:81) at com.sandiyu.lcd.DlpPrintActivity$PrintRunnable.clearImage(DlpPrintActivity.java:884) at com.sandiyu.lcd.DlpPrintActivity$PrintRunnable.access$1900(DlpPrintActivity.java:253) at com.sandiyu.lcd.DlpPrintActivity.onBackPressed(DlpPrintActivity.java:954) at android.app.Activity.onKeyUp(Activity.java:2193) ...
一般trace文件頂部的線程即為ANR的元兇,找到了犯罪線程我們就可以查看、分析一下犯罪現(xiàn)場(chǎng)。
- line 1,2
----- pid 879 at 2019-01-02 08:05:04 -----Cmd line: com.sandiyu.lcd
可以看到ANR 發(fā)生的進(jìn)程id,時(shí)間,名稱。
- line 3,4,5
JNI: CheckJNI is off; workarounds are off; pins=2; globals=273 DALVIK THREADS:(mutexes: tll=0 tsl=0 tscl=0 ghl=0)
可以看到線程的基本信息(tll:thread list lock,tsl:thread suspend lock,tscl:thread suspend count lock,ghl:gc heap lock)。
- line "main"
"main" prio=5 tid=1 WAIT
這一行說明了線程名稱,優(yōu)先級(jí),線程鎖id和線程狀態(tài)。可以看到本次ANR 線程為WAIT狀態(tài)。
額外補(bǔ)充一下線程狀態(tài)有如下幾種:
| java thread 狀態(tài) | cpp thread狀態(tài) | 說明 |
|---|---|---|
| TERMINATED | ZOMBIE | 線程死亡,終止運(yùn)行 |
| RUNNABLE | RUNNING/RUNNABLE | 線程可運(yùn)行或正在運(yùn)行 |
| TIMED_WAITING | TIMED_WAIT | 執(zhí)行了帶有超時(shí)參數(shù)的wait、sleep或join函數(shù) |
| BLOCKED | MONITOR | 線程阻塞,等待獲取對(duì)象鎖 |
| WAITING | WAIT | 執(zhí)行了無超時(shí)參數(shù)的wait函數(shù) |
| NEW | INITIALIZING | 新建,正在初始化,為其分配資源 |
| NEW | STARTING | 新建,正在啟動(dòng) |
| RUNNABLE | NATIVE | 正在執(zhí)行JNI本地函數(shù) |
| WAITING | VMWAIT | 正在等待VM資源 |
| RUNNABLE | SUSPENDED | 線程暫停,通常是由于GC或debug被暫停 |
| UNKNOWN | 未知狀態(tài) |
接著往下面的信息看
at com.sandiyu.lcd.utils.DeviceCommandSender$CommandSendThread.send(DeviceCommandSender.java:156) at com.sandiyu.lcd.utils.DeviceCommandSender.displayNull(DeviceCommandSender.java:81) at com.sandiyu.lcd.DlpPrintActivity$PrintRunnable.clearImage(DlpPrintActivity.java:884) at com.sandiyu.lcd.DlpPrintActivity$PrintRunnable.access$1900(DlpPrintActivity.java:253) at com.sandiyu.lcd.DlpPrintActivity.onBackPressed(DlpPrintActivity.java:954)
在這里我們就找到了原因,CommandSendThread.send需要等待網(wǎng)絡(luò)資源來更新UI,連接中斷了,這時(shí)候點(diǎn)擊onBackPressed長(zhǎng)時(shí)間得不到相應(yīng),它就報(bào)了ANR了。
六、造成ANR的原因及解決辦法
上面例子只是由于簡(jiǎn)單的主線程耗時(shí)操作造成的ANR,造成ANR的原因還有很多:
主線程阻塞或主線程數(shù)據(jù)讀取
解決辦法:避免死鎖的出現(xiàn),使用子線程來處理耗時(shí)操作或阻塞任務(wù)。盡量避免在主線程query provider、不要濫用SharePreferenceS
CPU滿負(fù)荷,I/O阻塞
解決辦法:文件讀寫或數(shù)據(jù)庫(kù)操作放在子線程異步操作。
內(nèi)存不足
解決辦法:AndroidManifest.xml文件中可以設(shè)置 android:largeHeap=“true”,以此增大App使用內(nèi)存。不過不建議使用此法,從根本上防止內(nèi)存泄漏,優(yōu)化內(nèi)存使用才是正道。
各大組件ANR
各大組件生命周期中也應(yīng)避免耗時(shí)操作,注意BroadcastReciever的onRecieve()、后臺(tái)Service和ContentProvider也不要執(zhí)行太長(zhǎng)時(shí)間的任務(wù)。