Android ANR的產生與分析

 

ANR即Application Not Responding應用無響應,通常在ANR的時候會彈出一個應用無響應對話框。也許有些開發者在使用某些手機開發中不在彈出應用無響應彈出框,特別是國產手機Android4.0以上的系統中,即便在開發者選項中設置了「顯示全部應用無響應-爲後臺應用顯示無響應ANR對話框」,主要是由於在某些國產手機系統中就將該選項屏蔽了,應用超過了必定時間無響應也不會彈出ANR對話框了。html

Android ANR的產生與分析

通常狀況下應用無響應的時候回產生一個日誌文件,位於/data/anr/文件夾下面,trace文件是Android Davik虛擬機在收到異常終止信號時產生的,最多見的一個觸發條件就是Android應用中產生了FC(force close)。因爲該文件的產生是在DVM中的,因此只有運行DVM實例的進程才能產生該文件,也就是說只有Java代碼才能產生該文件,App應用的Native層(如Android Library、用c/c++編譯的庫)即便異常也不會產生ANR日誌文件。咱們能夠經過ANR產生的traces日誌文件分析應用在哪裏產生了ANR,以此來有效解決應用中的ANR。java

Android ANR的產生與分析

爲何會產生ANR

在Android裏,應用程序的響應是由ActivityManager和WindowManager服務系統服務監視的,當檢測到下面三種狀況的任何一種時,Android就會針對特定的應用程序顯示ANR對話框。linux

  • Activity的UI在5秒內沒有響應輸入事件(例如,按鍵按下,屏幕觸摸)–主要類型
  • BroadcastReceiver在10秒內沒有執行完畢
  • Service在特定時間內(20秒內)沒法處理完成–小几率類型

形成ANR的緣由有不少,不管是在Activity或者BroadcastReceiver仍是在Service,咱們看到都是在主線程中操做引發的ANR,所以咱們應該避免在主線程作太多耗時的操做,網絡請求不用說了,Android4.0之後就禁止在主線程成執行請求了,除此以外就是要注意以下幾個方面:android

  • 主線程頻繁進行IO操做,好比讀寫文件或者數據庫;
  • 硬件操做如進行調用照相機或者錄音等操做;
  • 多線程操做的死鎖,致使主線程等待超時;
  • 主線程操做調用join()方法、sleep()方法或者wait()方法;
  • system server中發生WatchDog ANR;
  • service binder的數量達到上限。

traces.txt文件分析

若是拉取traces.txt日誌文件c++

當產生ANR的時候系統會生成一個日誌文件,日誌存放在/data/anr/文件夾下面,通常名稱爲traces.txt,可是也有例外的,以下:sql

Android ANR的產生與分析

若是手機已是徹底root的了,能夠直接經過DDMS的File Explorer直接導出來,若是不是root的手機,能夠經過以下adb命令查看ANR日誌文件位於哪裏。shell

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. 第1行是固定頭,指明下面都是當前運行的dvm thread:「DALVIK THREADS」;
  2. 第2行輸出的是改進程中各線程互斥量的值,有些手機上面可能沒有這一行日誌信息;
  3. 第3行輸出的是線程名字(「main」),線程優先級(「prio=5」),線程id(「tid=1」),線程狀態(Sleeping),比較常見的狀態還有Native、Waiting;
  4. 第4行分別是線程所處的線程組 (「main」),線程被正常掛起的次處(「sCount=1」),線程因調試而掛起次數(」dsCount=0「),當前線程所關聯的java線程對象(」obj=0x73f11000「)以及該線程自己的地址(「0xf3c25800」);
  5. 第5行 顯示線程調度信息,分別是該線程在linux系統下得本地線程id (「 sysTid=2957」),線程的調度有優先級(「nice=0」),調度策略(sched=0/0),優先組屬(「cgrp=default」)以及 處理函數地址(「handle=0xf7770ea0」);
  6. 第6行 顯示更多該線程當前上下文,分別是調度狀態(從 /proc/[pid]/task/[tid]/schedstat讀出)(「schedstat=( 107710942 40533261 131 )」),以及該線程運行信息 ,它們是線程用戶態下使用的時間值(單位是jiffies)(「utm=4」), 內核態下得調度時間值(「stm=6」),以及最後運行改線程的cup標識(「core=2」);
  7. 第7行表示線程棧的地址(「stack=0xff49d000-0xff49f000」)以及棧大小(「stackSize=8MB」);
  8. 後面是線程的調用棧信息,也是分析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行,代碼以下:

  1.  
    DBManager.getInstance().executeTask( new QueryExecutor() {
  2.  
    @Override
  3.  
    public void execute(SQLiteDatabasedatabase) {
  4.  
    //...
  5.  
    }
  6.  
    });
  7.  
    }

而後接着定位executeTask方法,方法定義以下:

  1.  
    public void executeTask(QueryExecutorexecutor) {
  2.  
    SQLiteDatabasedatabase = openDB();
  3.  
    executor.execute(database);
  4.  
    closeDB();
  5.  
    }

從這裏能夠看出來在下載過程當中咱們對數據庫的操做都是在Main線程中進行的,因此咱們能夠再定義一個異步的方法,將DownloadItemDao的updateDownload()方法中調用改成DBManager.getInstance().executeAsyncTask()的方式,這樣咱們將對數據庫這種比較耗時的操做子線程執行異步操做。

  1.  
    public void executeAsyncTask(final QueryExecutorexecutor) {
  2.  
    new Thread(new Runnable() {
  3.  
    public void run() {
  4.  
    SQLiteDatabasedatabase = openDB();
  5.  
    executor.execute(database);
  6.  
    closeDB();
  7.  
    }
  8.  
    }).start();
  9.  
    }

接着看一下下載中的回調方式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來實現。
  • 在設計及代碼編寫階段避免出現出現同步/死鎖或者錯誤處理不恰當等狀況。

參考資料

android ANR發生的緣由總結和解決辦法

如何分析解決Android ANR

Android 信號處理面面觀 之 trace 文件含義

Android ANR分析

android anr trace.txt文件 抓取

相關文章
相關標籤/搜索