Android ANR的產生與分析


 

ANR即Application Not Responding應用無響應,一般在ANR的時候會彈出一個應用無響應對話框。也許有些開發者在使用某些手機開發中不在彈出應用無響應彈出框,特別是國產手機Android4.0以上的系統中,即使在開發者選項中設置了“顯示所有應用無響應-為后台應用顯示無響應ANR對話框”,主要是因為在某些國產手機系統中就將該選項屏蔽了,應用超過了一定時間無響應也不會彈出ANR對話框了。

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。

Android 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,但是也有例外的,如下:

Android ANR的產生與分析

如果手機已經是完全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. 第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文件 抓取


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM