對一款A(yù)ndroid應(yīng)用來說,用戶體驗是至高無上的原則腥泥。如果應(yīng)用上手的體驗特別差,點點這點點那就出現(xiàn)各種問題讳嘱,用戶就會執(zhí)行最簡單的一個操作——卸載你的應(yīng)用栋烤。用戶體驗是一個非常大的概念,其中最直接粗暴的表現(xiàn)有兩個:崩潰和ANR股淡。這兩個問題是開發(fā)人員最不想看到的兩個現(xiàn)象身隐。對于崩潰來說,一般原因比較明顯唯灵,Android會在logcat中以紅色的字體打印出具體導致崩潰的代碼贾铝。對于線上應(yīng)用, 也有各種性能監(jiān)控工具可以幫我們分析埠帕。但是ANR的原因就比較難以定位忌傻。
ANR(Application Not Responding),即應(yīng)用程序無響應(yīng)搞监,相信很多安卓用戶都碰到過那個然并卵的對話框:
出現(xiàn)這個對話框,會讓用戶覺得很沮喪镰矿,輕則關(guān)閉琐驴,重則卸載。用戶就是這么一點一點流失的啊秤标。
產(chǎn)生ANR的原因
那么怎么避免出現(xiàn)ANR呢绝淡?首先要知道ANR產(chǎn)生的原因。
Android系統(tǒng)中產(chǎn)生ANR的原因大致分為三類:
- Service ANR苍姜,前臺進程中的Service生命周期超過20秒牢酵,后臺進程中的Service生命周期超過200秒時;
- Broadcast ANR衙猪,前臺的串行廣播消息中onReceive()方法執(zhí)行超過10秒馍乙,后臺的串行廣播消息中onReceive()方法執(zhí)行超過60秒時;
- Input ANR垫释,輸入時間在5秒內(nèi)無法響應(yīng)時丝格;
Android系統(tǒng)采用不同的監(jiān)測機制來監(jiān)測ANR,Service和Broadcast都是由AMS調(diào)度棵譬,利用Handler和Looper显蝌,設(shè)計了一個TIMEOUT消息交由AMS線程來處理,整個超時機制的實現(xiàn)都是在Java層订咸; InputEvent由InputDispatcher調(diào)度曼尊,待處理的輸入事件都會進入隊列中等待,設(shè)計了一個等待超時的判斷脏嚷,超時機制的實現(xiàn)在Native層骆撇。在這篇文章中做了很詳細的介紹。
具體到代碼層面父叙,是什么操作導致了以上情況的產(chǎn)生呢艾船?最重要的原因就是在主線程里執(zhí)行了太多的阻塞耗時操作葵腹。那么要避免ANR也就需要堅持一個原則:不要在主線程里做繁重的耗時操作。悲傷的是屿岂,我們的主工程里大量的數(shù)據(jù)庫操作都執(zhí)行在主線程里践宴,這是接下來(不是本文)需要深入探討的一個問題。
哪些操作時在主線程執(zhí)行的爷怀?
要做到不在主線程里執(zhí)行耗時操作阻肩,首先要知道哪些地方執(zhí)行的代碼是在主線程執(zhí)行的。
- Activity和Fragment里的所有生命周期回調(diào)都是執(zhí)行在主線程运授;
- Service默認是執(zhí)行在主線程的烤惊;
- BroadcastReceiver的onReceive()方法是執(zhí)行在主線程的;
- 沒有使用子線程looper的handler的handleMessage()方法和post(runnable)是執(zhí)行在主線程的吁朦;
- View的post(Runnbale)是執(zhí)行在主線程的柒室;
因此,在這些地方執(zhí)行操作時逗宜,要格外小心雄右,最保險的方法就是開啟一個子線程,把耗時操作都扔到子線程里做纺讲,具體創(chuàng)建一個異步操作擂仍,方法五花八門,這里不展開講了熬甚。
怎么分析ANR逢渔?
即使寫代碼的時候特別小心,也無法保證你的應(yīng)用不發(fā)生ANR乡括,因為不同的設(shè)備配置和性能上的差異肃廓,使得在A機器上4秒鐘執(zhí)行完的操作,在B機器上可能會花費超過4秒的時間诲泌,甚至是由于手機上的其他進程占用cpu導致cpu無法響應(yīng)當前應(yīng)用亿昏,種種原因都會導致ANR的出現(xiàn)。所以我們必須分析其產(chǎn)生的原因档礁,才能及時發(fā)現(xiàn)問題角钩,解決問題。
當ANR發(fā)生時呻澜,我們通常采用Logcat和traces.txt文件的相關(guān)信息去定位問題递礼。主要包含以下幾個方面:
- 基本信息,進程名羹幸、進程號脊髓、包名、系統(tǒng)build號栅受、ANR類型等将硝;
- cpu使用信息恭朗,活躍進程的cpu平均占用率、IO情況等依疼;
- 堆棧信息痰腮;
可以通過adb命令將traces.txt文件拉到本地:
$adb pull data/anr/traces.txt .
以下是病歷夾某一次ANR的日志:
----- pid 6603 at 2016-10-14 09:57:24 -----
Cmd line: com.apricotforest.dossier
JNI: CheckJNI is off; workarounds are off; pins=0; globals=412 (plus 20 weak)
DALVIK THREADS:
(mutexes: tll=0 tsl=0 tscl=0 ghl=0)
"main" prio=5 tid=1 TIMED_WAIT
| group="main" sCount=1 dsCount=0 obj=0x41744e58 self=0x41664910
| sysTid=6603 nice=-6 sched=0/0 cgrp=apps handle=1074172244
| state=S schedstat=( 28338231507 276030844958 49092 ) utm=2490 stm=343 core=3
at java.lang.VMThread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:1013)
at java.lang.Thread.sleep(Thread.java:995)
at com.apricotforest.dossier.persistentconnection.PersistentConnectionService$3.onClose(PersistentConnectionService.java:162)
at com.xingshulin.persistentconnection.WebSocketManager$1.onClose(WebSocketManager.java:31)
at de.tavendo.autobahn.WebSocketConnection.failConnection(WebSocketConnection.java:198)
at de.tavendo.autobahn.WebSocketConnection.access$7(WebSocketConnection.java:156)
at de.tavendo.autobahn.WebSocketConnection$1.handleMessage(WebSocketConnection.java:390)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5047)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:609)
at dalvik.system.NativeStart.main(Native Method)
...
從文中可以看到以上描述的一些信息,很明顯問題發(fā)生在長連接服務(wù)的onClose()方法中律罢,該方法中執(zhí)行的操作耗時導致發(fā)生了Service的ANR膀值。
上面這個例子比較簡單,日志里已經(jīng)很明白的告訴你出錯的位置了误辑,還有一些ANR問題發(fā)生的原因比較隱蔽沧踏,需要認真的分析,比如:
Process:com.apricotforest.dossier
...
CPU usage from 3330ms to 814ms ago:
6% 178/system_server: 3.5% user + 1.4% kernel / faults: 86 minor 20 major
4.6% 2976/com.apricotforest.dossier: 0.7% user + 3.7% kernel /faults: 52 minor 19 major
0.9% 252/com.android.systemui: 0.9% user + 0% kernel
...
100%TOTAL: 5.9% user + 4.1% kernel + 89% iowait
最后一句說明CPU占用100%巾钉,已經(jīng)滿負荷翘狱,其中大部分被iowait即I/O操作占用。這時候需要仔細的分析logcat和trace.txt,同時結(jié)合google砰苍,才有可能定位到問題原因潦匈。
p.p1 {margin: 0.0px 0.0px 0.0px 60.0px; font: 16.0px 'Heiti SC Light'; color: #586e75}p.p2 {margin: 0.0px 0.0px 0.0px 60.0px; font: 16.0px Courier; color: #586e75; min-height: 19.0px}p.p3 {margin: 0.0px 0.0px 0.0px 60.0px; font: 16.0px Courier; color: #859900}p.p4 {margin: 0.0px 0.0px 0.0px 60.0px; font: 16.0px Courier; color: #859900; min-height: 19.0px}span.s1 {font: 16.0px Courier}
內(nèi)存原因也可能導致ANR,例如用一張大圖片作為activity的背景师骗,trace信息可能是這樣的:
Cmdline: android.process.acore
DALVIK THREADS:
"main"prio=5 tid=3 VMWAIT
|group="main" sCount=1 dsCount=0 s=N obj=0x40026240self=0xbda8
| sysTid=1815 nice=0 sched=0/0 cgrp=unknownhandle=-1344001376
atdalvik.system.VMRuntime.trackExternalAllocation(NativeMethod)
atandroid.graphics.Bitmap.nativeCreate(Native Method)
atandroid.graphics.Bitmap.createBitmap(Bitmap.java:468)
atandroid.view.View.buildDrawingCache(View.java:6324)
atandroid.view.View.getDrawingCache(View.java:6178)
...
MEMINFO in pid 1360 [android.process.acore] **
native dalvik other total
size: 17036 23111 N/A 40147
allocated: 16484 20675 N/A 37159
free: 296 2436 N/A 2732
free的內(nèi)存已經(jīng)所剩無幾。當然讨惩,這時候發(fā)生OOM的幾率也變的很大了辟癌。
病歷夾的現(xiàn)狀:ANR比較頻繁,需要重點解決荐捻。