ANR即Application Not Responding應用無響應,一般在ANR的時候會彈出一個應用無響應對話框。也許有些開發者在使用某些手機開發中不在彈出應用無響應彈出框,特別是國產手機Android4.0以上的系統中,即使在開發者選項中設置了“顯示所有應用無響應-為后台應用顯示無響應ANR對話框”,主要是因為在某些國產手機系統中就將該選項屏蔽了,應用超過了一定時間無響應也不會彈出ANR對話框了。
一般情況下應用無響應的時候回產生一個日志文件,位於/data/anr/文件夾下面,trace文件是Android Davik虛擬機在收到異常終止信號時產生的,最常見的一個觸發條件就是Android應用中產生了FC(force close)。由於該文件的產生是在DVM中的,所以只有運行DVM實例的進程才能產生該文件,也就是說只有Java代碼才能產生該文件,App應用的Native層(如Android Library、用c/c++編譯的庫)即使異常也不會產生ANR日志文件。我們可以通過ANR產生的traces日志文件分析應用在哪里產生了ANR,以此來有效解決應用中的ANR。
為什么會產生ANR
在Android里,應用程序的響應是由ActivityManager和WindowManager服務系統服務監視的,當檢測到下面三種情況的任何一種時,Android就會針對特定的應用程序顯示ANR對話框。
- Activity的UI在5秒內沒有響應輸入事件(例如,按鍵按下,屏幕觸摸)–主要類型
- BroadcastReceiver在10秒內沒有執行完畢
- Service在特定時間內(20秒內)無法處理完成–小概率類型
造成ANR的原因有很多,無論是在Activity或者BroadcastReceiver還是在Service,我們看到都是在主線程中操作引起的ANR,因此我們應該避免在主線程做太多耗時的操作,網絡請求不用說了,Android4.0以后就禁止在主線程成執行請求了,除此之外就是要注意如下幾個方面:
- 主線程頻繁進行IO操作,比如讀寫文件或者數據庫;
- 硬件操作如進行調用照相機或者錄音等操作;
- 多線程操作的死鎖,導致主線程等待超時;
- 主線程操作調用join()方法、sleep()方法或者wait()方法;
- system server中發生WatchDog ANR;
- service binder的數量達到上限。
traces.txt文件分析
如果拉取traces.txt日志文件
當產生ANR的時候系統會生成一個日志文件,日志存放在/data/anr/文件夾下面,一般名稱為traces.txt,但是也有例外的,如下:
如果手機已經是完全root的了,可以直接通過DDMS的File Explorer直接導出來,如果不是root的手機,可以通過如下adb命令查看ANR日志文件位於哪里。
adb shell ls /data/anr/
然后通過adb的pull將日志文件拉取到指定的路徑。
adb pull /data/anr/traces.txt d:/
但是如果手機沒有進行root,執行adb pull命令就會出現如下提示:
remote object ‘/data/anr/traces.txt’ does not exist
這時候我們可以使用adb將文件copy一份到sdcard,然后再拉取出來。
adbshell
cat /data/anr/traces.txt >/mnt/sdcard/traces.txt exit
traces.txt日志文件分析
一般traces.txt日志輸出格式如下,本實例是在主線程中強行Sleep導致的ANR日志:
DALVIKTHREADS :
(mutexes: tll=0 tsl=0 tscl=0 ghl=0 hwl=0 hwll=0) "main" prio=5 tid=1 Sleeping | group="main" sCount=1 dsCount=0 obj=0x73f11000 self=0xf3c25800 | sysTid=2957 nice=0 cgrp=default sched=0/0 handle=0xf7770ea0 | state=S schedstat=( 107710942 40533261 131 ) utm=4 stm=6 core=2 HZ=100 | stack=0xff49d000-0xff49f000 stackSize=8MB | heldmutexes= atjava.lang.Thread.sleep!(Native method) - sleepingon <0x31fd6f5d> (a java.lang.Object) atjava.lang.Thread.sleep(Thread.java:1031) - locked <0x31fd6f5d> (a java.lang.Object) atjava.lang.Thread.sleep(Thread.java:985) atcom.sunny.demo.MainActivity.startMethod(MainActivity.java:21) atjava.lang.reflect.Method.invoke!(Native method) atjava.lang.reflect.Method.invoke(Method.java:372) atandroid.view.View$1.onClick(View.java:4015) atandroid.view.View.performClick(View.java:4780) atandroid.view.View$PerformClick.run(View.java:19866) atandroid.os.Handler.handleCallback(Handler.java:739) atandroid.os.Handler.dispatchMessage(Handler.java:95) atandroid.os.Looper.loop(Looper.java:135) atandroid.app.ActivityThread.main(ActivityThread.java:5254) atjava.lang.reflect.Method.invoke!(Native method) atjava.lang.reflect.Method.invoke(Method.java:372) atcom.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903) atcom.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
- 第1行是固定頭,指明下面都是當前運行的dvm thread:“DALVIK THREADS”;
- 第2行輸出的是改進程中各線程互斥量的值,有些手機上面可能沒有這一行日志信息;
- 第3行輸出的是線程名字(“main”),線程優先級(“prio=5”),線程id(“tid=1”),線程狀態(Sleeping),比較常見的狀態還有Native、Waiting;
- 第4行分別是線程所處的線程組 (“main”),線程被正常掛起的次處(“sCount=1”),線程因調試而掛起次數(”dsCount=0“),當前線程所關聯的java線程對象(”obj=0x73f11000“)以及該線程本身的地址(“0xf3c25800”);
- 第5行 顯示線程調度信息,分別是該線程在linux系統下得本地線程id (“ sysTid=2957”),線程的調度有優先級(“nice=0”),調度策略(sched=0/0),優先組屬(“cgrp=default”)以及 處理函數地址(“handle=0xf7770ea0”);
- 第6行 顯示更多該線程當前上下文,分別是調度狀態(從 /proc/[pid]/task/[tid]/schedstat讀出)(“schedstat=( 107710942 40533261 131 )”),以及該線程運行信息 ,它們是線程用戶態下使用的時間值(單位是jiffies)(“utm=4”), 內核態下得調度時間值(“stm=6”),以及最后運行改線程的cup標識(“core=2”);
- 第7行表示線程棧的地址(“stack=0xff49d000-0xff49f000”)以及棧大小(“stackSize=8MB”);
- 后面是線程的調用棧信息,也是分析ANR的核心所在。
通過traces.txt中 at com.sunny.demo.MainActivity.startMethod(MainActivity.java:21) 很容易就可以定位到我們的問題所在,MainActivity的第21行,然后我們可以看到代碼:
try {
Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); }
這就是由於主線程睡眠10s導致的無法響應。開發中定位ANR問題日志有個很簡單的規律,就是 直接找到我們自己開發App所使用的包名(包括第三方Library庫)信息開始定位找就可以了 。
再看另外一個實例,這是在開發中實際遇到的一個問題。
DALVIKTHREADS (23):
"main" prio=5 tid=1 Native | group="main" sCount=1 dsCount=0 obj=0x73f11000 self=0xf3c25800 | sysTid=2929 nice=0 cgrp=default sched=0/0 handle=0xf77bbea0 | state=R schedstat=( 42455718229 4950682716 114556 ) utm=208 stm=4036 core=1 HZ=100 | stack=0xff38d000-0xff38f000 stackSize=8MB | heldmutexes= atandroid.database.sqlite.SQLiteConnection.nativeExecute(Native method) atandroid.database.sqlite.SQLiteConnection.execute(SQLiteConnection.java:555) atandroid.database.sqlite.SQLiteSession.endTransactionUnchecked(SQLiteSession.java:437) atandroid.database.sqlite.SQLiteSession.endTransaction(SQLiteSession.java:401) atandroid.database.sqlite.SQLiteDatabase.endTransaction(SQLiteDatabase.java:522) atcom.xxx.xxxx.db.dao.DownloadItemDao$2.execute(DownloadItemDao.java:171) atcom.xxx.xxxx.db.DBManager.executeTask(DBManager.java:44) atcom.xxx.xxxx.db.dao.DownloadItemDao.updateDownload(DownloadItemDao.java:154) atcom.xxx.xxxx.download.DownloadManager$ManagerListener.onUIProgress(DownloadManager.java:222) atcom.sunny.net.listener.impl.UIProgressListener$UIHandler.progress(UIProgressListener.java:30) atcom.sunny.net.listener.handler.ProgressHandler.handleMessage(ProgressHandler.java:47) atandroid.os.Handler.dispatchMessage(Handler.java:102) atandroid.os.Looper.loop(Looper.java:135) atandroid.app.ActivityThread.main(ActivityThread.java:5254) atjava.lang.reflect.Method.invoke!(Native method) atjava.lang.reflect.Method.invoke(Method.java:372) atcom.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903) atcom.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
先定位數據庫操作處的問題,在DownloadItemDao的updateDownload()方法的154行,代碼如下:
-
DBManager.getInstance().executeTask( new QueryExecutor() {
-
-
public void execute(SQLiteDatabasedatabase) {
-
//...
-
}
-
});
-
}
然后接着定位executeTask方法,方法定義如下:
-
public void executeTask(QueryExecutorexecutor) {
-
SQLiteDatabasedatabase = openDB();
-
executor.execute(database);
-
closeDB();
-
}
從這里可以看出來在下載過程中我們對數據庫的操作都是在Main線程中進行的,所以我們可以再定義一個異步的方法,將DownloadItemDao的updateDownload()方法中調用改為DBManager.getInstance().executeAsyncTask()的方式,這樣我們將對數據庫這種比較耗時的操作子線程執行異步操作。
-
public void executeAsyncTask(final QueryExecutorexecutor) {
-
new Thread(new Runnable() {
-
public void run() {
-
SQLiteDatabasedatabase = openDB();
-
executor.execute(database);
-
closeDB();
-
}
-
}).start();
-
}
接着看一下下載中的回調方式at com.sunny.net.listener.handler.ProgressHandler.handleMessage(ProgressHandler.java:47),在ProgressHandler類的47行,我們看到這里是將子線程異步下載的信息實時回調到主線程的地方:
if (!progressModel.isDone()) {
progress(uiProgessListener, progressModel.getCurrentBytes(), progressModel.getContentLength(), progressModel.isDone());
}
這里的實現方式毫無疑問是實時回調進度,在UI展現層進度可以不必實時回調,可以減少對頻繁UI更新操作,從分析可以知道頻繁更新UI也是導致ANR的一個很重要的原因,因此可以適當的采用一定的延時再執行UI回調,更改后代碼如下:
public static final int MIN_RATE=1000; long currTime = SystemClock.uptimeMillis(); if (currTime - lastUpdateTime >= MIN_RATE&&!progressModel.isDone()) { lastUpdateTime = currTime; progress(uiProgessListener, progressModel.getCurrentBytes(), progressModel.getContentLength(), progressModel.isDone()); }
這樣通過ANR日志文件可以非常方面快捷的定位到問題所在,並及時的給出解決方式提高日常開發中對問題的處理效率。
如何避免ANR
- 避免在主線程進行復雜耗時的操作,特別是文件讀取或者數據庫操作;
- 避免頻繁實時更新UI;
- BroadCastReceiver 要進行復雜操作的的時候,可以在onReceive()方法中啟動一個Service來處理;
- 避免在IntentReceiver里啟動一個Activity,因為它會創建一個新的畫面,並從當前用戶正在運行的程序上搶奪焦點。如果你的應用程序在響應Intent廣 播時需要向用戶展示什么,你應該使用Notification Manager來實現。
- 在設計及代碼編寫階段避免出現出現同步/死鎖或者錯誤處理不恰當等情況。