1.什么是ANR
ANR(Application Not responding)是指應(yīng)用程序未響應(yīng)课梳,主線程如果在規(guī)定時(shí)間內(nèi)沒(méi)有處理完相應(yīng)的工作理卑,就會(huì)出現(xiàn)ANR翘紊,ANR本質(zhì)上是性能問(wèn)題,ANR機(jī)制實(shí)際上是對(duì)應(yīng)用程序的主線程起到了限制作用藐唠。
2.ANR產(chǎn)生的原因是什么帆疟?
主線程(UI線程)如果在規(guī)定時(shí)間內(nèi)沒(méi)有處理完相應(yīng)的工作鹉究,就會(huì)出現(xiàn)ANR,具體的說(shuō)就是:
1. 輸入事件(按鍵和觸摸事件)5s內(nèi)沒(méi)有被處理:Input event dispatching timed out
2. BroadcaseReceiver的事件(onReceive方法)在規(guī)定時(shí)間內(nèi)沒(méi)處理完(前臺(tái)廣播10s,后臺(tái)廣播60s):Timeout of broadcast BroadcastRecord
3. service前臺(tái)20s,后臺(tái)200s沒(méi)有完成啟動(dòng) :Timeout executing service
4. ContentProvider的publish在10s內(nèi)沒(méi)進(jìn)行完:Timeout publishing content providers
3.ANR發(fā)生的原理 -- 機(jī)制
ANR機(jī)制可以分為兩部分:
- ANR監(jiān)測(cè)機(jī)制:Android對(duì)于不同的ANR類型(Broadcast, Service, InputEvent)都有一套監(jiān)測(cè)機(jī)制踪宠。
- ANR報(bào)告機(jī)制:在監(jiān)測(cè)到ANR以后自赔,需要顯示ANR對(duì)話框、輸出日志(發(fā)生ANR時(shí)的進(jìn)程函數(shù)調(diào)用棧柳琢、CPU使用情況等)绍妨。
整個(gè)ANR機(jī)制的代碼也是橫跨了Android的幾個(gè)層:
- App層:應(yīng)用主線程的處理邏輯;
- Framework層:ANR機(jī)制的核心柬脸,主要有AMS他去、BroadcastQueue、ActiveServices倒堕、InputmanagerService灾测、InputMonitor、InputChannel垦巴、ProcessCpuTracker等媳搪;
- Native層:InputDispatcher.cpp;
直白的說(shuō)就是程序在執(zhí)行相關(guān)操作的時(shí)候骤宣,會(huì)通過(guò)handler發(fā)送一個(gè)延時(shí)消息秦爆,也就是ANR消息,延時(shí)的時(shí)間就是不同組件發(fā)生ANR的時(shí)間涯雅,當(dāng)我們進(jìn)行相關(guān)操作之后鲜结,會(huì)romove掉該條message,如果相關(guān)的操作沒(méi)有在規(guī)定的時(shí)間內(nèi)完成活逆,那么Handler就會(huì)執(zhí)行該條message精刷,就發(fā)生的ANR。
4.ANR定位 - 如何找到ANR發(fā)生的原因
發(fā)生ANR的原因有以下幾點(diǎn):
- 主線程在做一些耗時(shí)任務(wù)
- 主線程被其他線程鎖
- cpu被其他進(jìn)程占用蔗候,該進(jìn)程沒(méi)被分配到足夠的cpu資源
如何準(zhǔn)確的找到ANR發(fā)生的位置怒允,就需要我們結(jié)合具體的場(chǎng)景進(jìn)行分析,具體可以從以下幾個(gè)方面進(jìn)行分析
可以在log中搜索“ANR in”或“am_anr”锈遥,會(huì)找到ANR發(fā)生的log纫事,該行會(huì)包含了ANR的時(shí)間、進(jìn)程所灸、是何種ANR等信息丽惶,如果是BroadcastReceiver的ANR,可以懷疑是BroadcastReceiver.onReceive()方法的問(wèn)題爬立。如果是Service或Provider就懷疑是否其onCreate()的問(wèn)題钾唬。
在該條log之后會(huì)有CPU usage的信息,表明了CPU在ANR前后的用量(log會(huì)表明截取ANR的時(shí)間),從各種CPU Usage信息中大概可以分析如下幾點(diǎn):
(1). 如果某些進(jìn)程的CPU占用百分比較高抡秆,幾乎占用了所有CPU資源奕巍,而發(fā)生ANR的進(jìn)程CPU占用為0%或非常低,則認(rèn)為CPU資源被占用儒士,進(jìn)程沒(méi)有被分配足夠的資源的止,從而發(fā)生了ANR。這種情況多數(shù)可以認(rèn)為是系統(tǒng)狀態(tài)的問(wèn)題着撩,并不是由本應(yīng)用造成的诅福。
(2). 如果發(fā)生ANR的進(jìn)程CPU占用較高,如到了80%或90%以上拖叙,則可以懷疑應(yīng)用內(nèi)一些代碼不合理消耗掉了CPU資源权谁,如出現(xiàn)了死循環(huán)或者后臺(tái)有許多線程執(zhí)行任務(wù)等等原因,這就要結(jié)合trace和ANR前后的log進(jìn)一步分析了憋沿。
(3). 如果CPU總用量不高旺芽,該進(jìn)程和其他進(jìn)程的占用過(guò)高,這有一定概率是由于某些主線程的操作就是耗時(shí)過(guò)長(zhǎng)辐啄,或者是由于主進(jìn)程被鎖造成的采章。除了上述的情況(1)以外,分析CPU usage之后壶辜,確定問(wèn)題需要我們進(jìn)一步分析trace文件悯舟。trace文件記錄了發(fā)生ANR前后該進(jìn)程的各個(gè)線程的stack。對(duì)我們分析ANR問(wèn)題最有價(jià)值的就是其中主線程的stack砸民,一般主線程的trace可能有如下幾種情況:
(1). 主線程是running或者native而對(duì)應(yīng)的棧對(duì)應(yīng)了我們應(yīng)用中的函數(shù)抵怎,則很有可能就是執(zhí)行該函數(shù)時(shí)候發(fā)生了超時(shí)。
(2). 主線程被block:非常明顯的線程被鎖岭参,這時(shí)候可以看是被哪個(gè)線程鎖了反惕,可以考慮優(yōu)化代碼。如果是死鎖問(wèn)題演侯,就更需要及時(shí)解決了姿染。
(3). 由于抓trace的時(shí)刻很有可能耗時(shí)操作已經(jīng)執(zhí)行完了(ANR -> 耗時(shí)操作執(zhí)行完畢 ->系統(tǒng)抓trace),這時(shí)候的trace就沒(méi)有什么用了秒际,主線程的stack就是這樣的:
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 obj=0x757855c8 self=0xb4d76500
| sysTid=3276 nice=0 cgrp=default sched=0/0 handle=0xb6ff5b34
| state=S schedstat=( 50540218363 186568972172 209049 ) utm=3290 stm=1764 core=3 HZ=100
| stack=0xbe307000-0xbe309000 stackSize=8MB
| held mutexes=
kernel: (couldn't read /proc/self/task/3276/stack)
native: #00 pc 0004099c /system/lib/libc.so (__epoll_pwait+20)
native: #01 pc 00019f63 /system/lib/libc.so (epoll_pwait+26)
native: #02 pc 00019f71 /system/lib/libc.so (epoll_wait+6)
native: #03 pc 00012ce7 /system/lib/libutils.so (_ZN7android6Looper9pollInnerEi+102)
native: #04 pc 00012f63 /system/lib/libutils.so (_ZN7android6Looper8pollOnceEiPiS1_PPv+130)
native: #05 pc 00086abd /system/lib/libandroid_runtime.so (_ZN7android18NativeMessageQueue8pollOnceEP7_JNIEnvP8_jobjecti+22)
native: #06 pc 0000055d /data/dalvik-cache/arm/system@framework@boot.oat (Java_android_os_MessageQueue_nativePollOnce__JI+96)
at android.os.MessageQueue.nativePollOnce(Native method)
at android.os.MessageQueue.next(MessageQueue.java:323)
at android.os.Looper.loop(Looper.java:138)
at android.app.ActivityThread.main(ActivityThread.java:5528)
at java.lang.reflect.Method.invoke!(Native method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:740)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:630)
當(dāng)然這種情況很有可能是由于該進(jìn)程的其他線程消耗掉了CPU資源悬赏,這就需要分析其他線程的trace以及ANR前后該進(jìn)程自己輸出的log了。
5.ANR的修正 - 如何避免ANR
- 主線程讀取數(shù)據(jù):在Android中主線程去讀取數(shù)據(jù)是非常不友好的娄徊,Android從2.3之后是不允許主線程從網(wǎng)絡(luò)讀取數(shù)據(jù)的闽颇,但系統(tǒng)允許主線程從數(shù)據(jù)庫(kù)或者其他地方讀取數(shù)據(jù),但是這種情況就會(huì)容易產(chǎn)生ANR寄锐。
- 避免在主線程執(zhí)行query provider兵多,首先這會(huì)比較耗時(shí)捻脖,另外這個(gè)操作provider那一方如果掛掉的話或者正在啟動(dòng),那我們應(yīng)用的query就會(huì)很長(zhǎng)時(shí)間不會(huì)返回中鼠,我們應(yīng)該在其他線程中執(zhí)行數(shù)據(jù)庫(kù)的query,provider的query等獲取數(shù)據(jù)的操作沿癞。
- sharePreference的調(diào)用援雇。sharePreference的commit()方法是同步的,apply()方法一般是異步執(zhí)行椎扬。所以在主線程不要用commit()方法惫搏,用apply()替換。SharedPreference的寫(xiě)是全量寫(xiě)而非增量寫(xiě)蚕涤。所以盡量都修改完然后再調(diào)用apply()筐赔,apply()方法再Activity stop的時(shí)候,主線程會(huì)等待寫(xiě)入完成揖铜,提交多次就會(huì)容易造成卡頓茴丰。并且存儲(chǔ)文本不宜過(guò)大,這樣會(huì)很慢天吓。另外贿肩,如果寫(xiě)入的是json或者xml,由于需要加和刪轉(zhuǎn)義符號(hào)龄寞,速度會(huì)比較慢汰规。
- 盡量避免在BroadcastReceiver的onReceive()方法中執(zhí)行操作,特別是應(yīng)用在后臺(tái)的時(shí)候物邑,為了避免這種情況溜哮,一種解決方案是直接開(kāi)啟異步線程去執(zhí)行任務(wù),但是此時(shí)應(yīng)用可能在后臺(tái)色解,系統(tǒng)優(yōu)先級(jí)較低茂嗓,進(jìn)程很容易就被殺死了,所以可以選擇開(kāi)啟Service去執(zhí)行任務(wù)科阎。
- 各個(gè)組件的生命周期函數(shù)都不應(yīng)該有太耗時(shí)的操作在抛,即使對(duì)于后臺(tái)Service或者ContentProvider來(lái)講,應(yīng)用在后臺(tái)運(yùn)行時(shí)候其onCreate()時(shí)候不會(huì)有用戶輸入引起事件無(wú)響應(yīng)ANR萧恕,但其執(zhí)行時(shí)間過(guò)長(zhǎng)也會(huì)引起Service的ANR和ContentProvider的ANR刚梭。
- 盡量避免主線程的被鎖的情況,在一些同步的操作主線程有可能被鎖票唆,需要等待其他線程釋放相應(yīng)鎖才能繼續(xù)執(zhí)行朴读,這樣會(huì)有一定的ANR風(fēng)險(xiǎn),對(duì)于這種情況有時(shí)也可以用異步線程來(lái)執(zhí)行相應(yīng)的邏輯走趋。另外衅金, 我們要避免死鎖的發(fā)生(主線程被死鎖基本就等于要發(fā)生ANR了)。