ANR異常

1 ANR定義

ANR即應(yīng)用無響應(yīng)(application not responding),一般會彈出無響應(yīng)提示對話框,某些系統(tǒng)可能會屏蔽掉彈框功能。ANR的日志存放在/data/anr/下,此日志只記錄VM層面的日志,不會記錄Native的行為日志;

1.1 ANR滿足條件與情況

ANR的產(chǎn)生需要同時滿足三個條件:

  • 主線程:只有應(yīng)用程序進程的主線程響應(yīng)超時才會產(chǎn)生ANR;
  • 超時時間:產(chǎn)生ANR的上下文不同,超時時間也不同,但只要超過這個時間上限沒有響應(yīng)就會產(chǎn)生ANR;
  • 輸入事件/特定操作:輸入事件是指按鍵、觸屏等設(shè)備輸入事件,特定操作是指BroadcastReceiver和Service的生命周期中的各個函數(shù)調(diào)用。

產(chǎn)生ANR的上下文不同,導致ANR原因也不同,主要有以下三種情況:

  • InputDispatchingTimeout
    主線程對5秒內(nèi)無法響應(yīng)屏幕觸摸事件或鍵盤輸入事件;

  • BroadcastQueueTimeout
    BroadcastReceiver的onReceive()函數(shù)運行在主線程中,在特定的時間(10秒)內(nèi)無法完成處理,后臺進程超時時間是60秒。

  • ServiceTimeout
    比較少出現(xiàn)的一種類型,原因是Service的各個生命周期函數(shù)在特定時間(20秒)內(nèi)無法完成處理。

2 ANR原理

2.1 ANR的產(chǎn)生流程

具體代碼分析參考鏈接[3]
ANR 產(chǎn)生的流程如下所示:


ANR產(chǎn)生流程.png
  • 判斷窗口是否可以分發(fā)事件
    • 窗口處于 paused 狀態(tài), 則無法分發(fā)事件
    • 如果窗口的連接沒有注冊, 則無法分發(fā)事件
    • 窗口連接失效, 則無法分發(fā)事件
    • 窗口的事件待處理隊列 outboundQueue 已滿, 則無法分發(fā)事件
    • 若為 Key Event, 窗口的 outboundQueue 或 waitQueue 有數(shù)據(jù), 則無法分發(fā)事件
    • 若非 Key Event, 窗口 waitQueue 非空 且 頭事件分發(fā)超時 500ms, 則無法分發(fā)事件
  • 若不能分發(fā), 則安放定時器, 默認 5s 之后重試
    • 在此期間若是分發(fā)成功之后會重置定時器
  • 重試的時候, 若仍然分發(fā)失敗, 則調(diào)用 onANRLocked 彈出 ANR 彈窗

2.2 ANR的彈出

具體代碼分析參考鏈接[3]
產(chǎn)生ANR對話框流程:

  1. onANRLocked 從 Native 層回溯到 Java 層的 IMS
  2. IMS 交由 AMS 處理 ANR
  3. AMS 處理 ANR
    • 在 ServiceThread 線程 dump ANR 信息
    • 在 UIThread 線程彈出 AppErrorDialog

2.3 ANR的信息收集

這部分代碼位于ActivityManagerService類中:

final void appNotResponding(ProcessRecord app, ActivityRecord activity,
            ActivityRecord parent, boolean aboveSystem, final String annotation) {
        ArrayList<Integer> firstPids = new ArrayList<Integer>(5);
        SparseArray<Boolean> lastPids = new SparseArray<Boolean>(20);
        // mController是IActivityController接口的實例,是為Monkey測試程序預留的,默認為null
        if (mController != null) {
            try {
                // 0 == continue, -1 = kill process immediately
                int res = mController.appEarlyNotResponding(app.processName, app.pid, annotation);
                if (res < 0 && app.pid != MY_PID) {
                    app.kill("anr", true);
                }
            } catch (RemoteException e) {
                mController = null;
                Watchdog.getInstance().setActivityController(null);
            }
        }
        // 更新CPU狀態(tài)信息
        long anrTime = SystemClock.uptimeMillis();
        if (MONITOR_CPU_USAGE) {
            updateCpuStatsNow();
        }
        synchronized (this) {
            // 某些特定情況下忽略本次ANR,比如系統(tǒng)關(guān)機,比如該進程已經(jīng)處于anr狀態(tài)或者crash狀態(tài)
            if (mShuttingDown) {
                Slog.i(TAG, "During shutdown skipping ANR: " + app + " " + annotation);
                return;
            } else if (app.notResponding) {
                Slog.i(TAG, "Skipping duplicate ANR: " + app + " " + annotation);
                return;
            } else if (app.crashing) {
                Slog.i(TAG, "Crashing app skipping ANR: " + app + " " + annotation);
                return;
            }
            // 為了防止多次對相同app的anr執(zhí)行重復代碼,在此處標注記錄,屬于上面的特定情況種的一種
            app.notResponding = true;
            // 記錄ANR信息到Event Log中
            EventLog.writeEvent(EventLogTags.AM_ANR, app.userId, app.pid,
                    app.processName, app.info.flags, annotation);
            // 添加當前app到firstpids列表中
            firstPids.add(app.pid);
            // 如果可能添加父進程到firstpids列表種
            int parentPid = app.pid;
            if (parent != null && parent.app != null && parent.app.pid > 0) parentPid = parent.app.pid;
            if (parentPid != app.pid) firstPids.add(parentPid);
            if (MY_PID != app.pid && MY_PID != parentPid) firstPids.add(MY_PID);
            // 添加所有進程到firstpids中
            for (int i = mLruProcesses.size() - 1; i >= 0; i--) {
                ProcessRecord r = mLruProcesses.get(i);
                if (r != null && r.thread != null) {
                    int pid = r.pid;
                    if (pid > 0 && pid != app.pid && pid != parentPid && pid != MY_PID) {
                        if (r.persistent) {
                            firstPids.add(pid);
                        } else {
                            lastPids.put(pid, Boolean.TRUE);
                        }
                    }
                }
            }
        }
        // 將ANR信息存在info變量中,后續(xù)打印到LOGCAT,這部分的信息會以ActivityManager為Tag打印出來,包含了ANR的進程,出現(xiàn)原因以及當時的CPU狀態(tài),這些對分析ANR是非常重要的信息
        StringBuilder info = new StringBuilder();
        info.setLength(0);
        info.append("ANR in ").append(app.processName);
        if (activity != null && activity.shortComponentName != null) {
            info.append(" (").append(activity.shortComponentName).append(")");
        }
        info.append("\n");
        info.append("PID: ").append(app.pid).append("\n");
        if (annotation != null) {
            info.append("Reason: ").append(annotation).append("\n");
        }
        if (parent != null && parent != activity) {
            info.append("Parent: ").append(parent.shortComponentName).append("\n");
        }
        final ProcessCpuTracker processCpuTracker = new ProcessCpuTracker(true);
        // 將ANR信息輸出到traces文件
        File tracesFile = dumpStackTraces(true, firstPids, processCpuTracker, lastPids,
                NATIVE_STACKS_OF_INTEREST);
        String cpuInfo = null;
        if (MONITOR_CPU_USAGE) {
            updateCpuStatsNow();
            synchronized (mProcessCpuTracker) {
                cpuInfo = mProcessCpuTracker.printCurrentState(anrTime);
            }
            info.append(processCpuTracker.printCurrentLoad());
            info.append(cpuInfo);
        }
        info.append(processCpuTracker.printCurrentState(anrTime));
        // 輸出到logcat的語句
        Slog.e(TAG, info.toString());
        // 如果traces文件未創(chuàng)建,則只記錄當前進程trace并且發(fā)送QUIT信號到進程
        if (tracesFile == null) {
            // There is no trace file, so dump (only) the alleged culprit's threads to the log
            Process.sendSignal(app.pid, Process.SIGNAL_QUIT);
        }
        // 將ANR信息處處到DropBox目錄下,也就是說除了traces文件還會有一個dropbox文件用于記錄ANR
        addErrorToDropBox("anr", app, app.processName, activity, parent, annotation,
                cpuInfo, tracesFile, null);
        //...
        synchronized (this) {
            mBatteryStatsService.noteProcessAnr(app.processName, app.uid);
            if (!showBackground && !app.isInterestingToUserLocked() && app.pid != MY_PID) {
                app.kill("bg anr", true);
                return;
            }
            // Set the app's notResponding state, and look up the errorReportReceiver
            makeAppNotRespondingLocked(app,
                    activity != null ? activity.shortComponentName : null,
                    annotation != null ? "ANR " + annotation : "ANR",
                    info.toString());
            // 發(fā)送SHOW_NOT_RESPONDING_MSG,準備顯示ANR對話框
            Message msg = Message.obtain();
            HashMap<String, Object> map = new HashMap<String, Object>();
            msg.what = SHOW_NOT_RESPONDING_MSG;
            msg.obj = map;
            msg.arg1 = aboveSystem ? 1 : 0;
            map.put("app", app);
            if (activity != null) {
                map.put("activity", activity);
            }
            mUiHandler.sendMessage(msg);
        }
    }

當發(fā)生ANR時, 會按順序依次執(zhí)行:

  1. 輸出ANR Reason信息到EventLog. 也就是說ANR觸發(fā)的時間點最接近的就是EventLog中輸出的am_anr信息;
  2. 收集并輸出重要進程列表中的各個線程的traces信息,該方法較耗時;
  3. 輸出當前各個進程的CPU使用情況以及CPU負載情況;
  4. 將traces文件和 CPU使用情況信息保存到dropbox,即data/system/dropbox目錄;
  5. 根據(jù)進程類型,來決定直接后臺殺掉,還是彈框告知用戶;、

ANR輸出重要進程的traces信息,這些進程包含:

  • firstPids隊列:第一個是ANR進程,第二個是system_server,剩余是所有persistent進程;
  • Native隊列:是指/system/bin/目錄的mediaserver,sdcard 以及surfaceflinger進程;
  • lastPids隊列:是指mLruProcesses中的不屬于firstPids的所有進程;

具體詳細過程參看:參考鏈接[4]

3 產(chǎn)生ANR常見原因及解決方案

  • 主線程阻塞
    解決辦法:避免死鎖的出現(xiàn),使用子線程來處理耗時操作或阻塞任務(wù),避免主線程調(diào)用join(),sleep()或wait()方法;應(yīng)用程序的UI線程等待子線程釋放某個鎖,從而無法處理用戶的輸入。
  • I/O操作
    解決方法:盡量避免在主線程文件讀取或數(shù)據(jù)庫操作、不要濫用SharePreferences。
  • 頻繁刷新UI
    解決辦法:避免頻繁實時刷新UI,如下載進度實時更新,可以進行采樣方式降低更新頻率;
  • 各大組件ANR
    解決辦法:BroadCastReceiver不要進行復雜操作的操作,可以在onReceive()方法中啟動一個Service來處理;避免在Intent Receiver里啟動一個Activity,因為它會創(chuàng)建一個新的畫面,并從當前用戶正在運行的程序上搶奪焦點。如果你的應(yīng)用程序在響應(yīng)Intent廣 播時需要向用戶展示什么,你應(yīng)該使用Notification Manager來實現(xiàn)。

4 ANR分析

ANR可以生成traces.txt以及DropBox目錄下的ANR歷史記錄,因此可以考慮閱讀該文件來分析,除此之外我們還有DDMS幫助我們分析ANR,這兩種方式實際上是大同小異的,只是應(yīng)用的場景不同。在講ANR分析之前,先看看Java應(yīng)用的分析。

4.1 Java線程調(diào)用分析方法

為什么要在講Android的ANR分析方法之前提到Java的分析方法呢,因為需要在解釋ANR之前稍微介紹一下線程狀態(tài)的概念,以便后面做敘述,同時也可以更方便的帶入分析的方法。JDK中有一個關(guān)鍵命令可以幫助我們分析和調(diào)試Java應(yīng)用——jstack,命令的使用方法是:

jstack {pid}

其中pid可以通過jps命令獲得,jps命令會列出當前系統(tǒng)中運行的所有Java虛擬機進程,比如這樣:

> jps
7266 Test
7267 Jps

上面的命令可以發(fā)現(xiàn)系統(tǒng)中目前有7266和7267兩個Java虛擬機進程,此時如果想知道當前Test進程的情況,就可以通過jstack命令來查看。jstack命令的輸出結(jié)果很簡單,它會打印出該進程中所有線程的狀態(tài)以及調(diào)用關(guān)系,甚至會給出一些簡單的分析結(jié)果:

2016-06-20 14:01:54
Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.71-b01 mixed mode):
"Attach Listener" daemon prio=5 tid=0x00007fde7385d800 nid=0x3507 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
"DestroyJavaVM" prio=5 tid=0x00007fde73873000 nid=0x1303 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
"Thread-1" prio=5 tid=0x00007fde73872800 nid=0x4a03 waiting for monitor entry [0x000000011cb30000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at Test.rightLeft(Test.java:48)
    - waiting to lock <0x00000007d56540a0> (a Test$LeftObject)
    - locked <0x00000007d5656180> (a Test$RightObject)
    at Test$2.run(Test.java:68)
    at java.lang.Thread.run(Thread.java:745)
"Thread-0" prio=5 tid=0x00007fde73871800 nid=0x4803 waiting for monitor entry [0x000000011ca2d000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at Test.leftRight(Test.java:34)
    - waiting to lock <0x00000007d5656180> (a Test$RightObject)
    - locked <0x00000007d56540a0> (a Test$LeftObject)
    at Test$1.run(Test.java:60)
    at java.lang.Thread.run(Thread.java:745)
"Service Thread" daemon prio=5 tid=0x00007fde73821000 nid=0x4403 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" daemon prio=5 tid=0x00007fde73035000 nid=0x4203 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" daemon prio=5 tid=0x00007fde7381e000 nid=0x4003 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" daemon prio=5 tid=0x00007fde7481d800 nid=0x300f runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
"Finalizer" daemon prio=5 tid=0x00007fde73010000 nid=0x2d03 in Object.wait() [0x000000011aacb000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x00000007d55047f8> (a java.lang.ref.ReferenceQueue$Lock)
    at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:135)
    - locked <0x00000007d55047f8> (a java.lang.ref.ReferenceQueue$Lock)
    at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:151)
    at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
"Reference Handler" daemon prio=5 tid=0x00007fde7300f000 nid=0x2b03 in Object.wait() [0x000000011a9c8000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x00000007d5504410> (a java.lang.ref.Reference$Lock)
    at java.lang.Object.wait(Object.java:503)
    at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:133)
    - locked <0x00000007d5504410> (a java.lang.ref.Reference$Lock)
"VM Thread" prio=5 tid=0x00007fde7300c800 nid=0x2903 runnable
"GC task thread#0 (ParallelGC)" prio=5 tid=0x00007fde74000800 nid=0x2103 runnable
"GC task thread#1 (ParallelGC)" prio=5 tid=0x00007fde7400c000 nid=0x2303 runnable
"GC task thread#2 (ParallelGC)" prio=5 tid=0x00007fde7400c800 nid=0x2503 runnable
"GC task thread#3 (ParallelGC)" prio=5 tid=0x00007fde7400d000 nid=0x2703 runnable
"VM Periodic Task Thread" prio=5 tid=0x00007fde7481e000 nid=0x4603 waiting on condition
JNI global references: 110
 
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007fde73818ab8 (object 0x00000007d56540a0, a Test$LeftObject),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007fde73819f58 (object 0x00000007d5656180, a Test$RightObject),
  which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
    at Test.rightLeft(Test.java:48)
    - waiting to lock <0x00000007d56540a0> (a Test$LeftObject)
    - locked <0x00000007d5656180> (a Test$RightObject)
    at Test$2.run(Test.java:68)
    at java.lang.Thread.run(Thread.java:745)
"Thread-0":
    at Test.leftRight(Test.java:34)
    - waiting to lock <0x00000007d5656180> (a Test$RightObject)
    - locked <0x00000007d56540a0> (a Test$LeftObject)
    at Test$1.run(Test.java:60)
    at java.lang.Thread.run(Thread.java:745)
Found 1 deadlock.
4.1.1 Thread基礎(chǔ)信息

輸出種包含所有的線程,取其中的一條:

"Thread-1" prio=5 tid=0x00007fde73872800 nid=0x4a03 waiting for monitor entry [0x000000011cb30000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at Test.rightLeft(Test.java:48)
    - waiting to lock <0x00000007d56540a0> (a Test$LeftObject)
    - locked <0x00000007d5656180> (a Test$RightObject)
    at Test$2.run(Test.java:68)
    at java.lang.Thread.run(Thread.java:745)
  1. "Thread-1" prio=5 tid=0x00007fde73872800 nid=0x4a03 waiting for monitor entry [0x000000011cb30000]
    首先描述了線程名是『Thread-1』,然后prio=5表示優(yōu)先級,tid表示的是線程id,nid表示native層的線程id,他們的值實際都是一個地址,后續(xù)給出了對于線程狀態(tài)的描述,waiting for monitor entry [0x000000011cb30000]這里表示該線程目前處于一個等待進入臨界區(qū)狀態(tài),該臨界區(qū)的地址是[0x000000011cb30000]
    這里對線程的描述多種多樣,簡單解釋下上面出現(xiàn)的幾種狀態(tài)
  • waiting on condition(等待某個事件出現(xiàn))
  • waiting for monitor entry(等待進入臨界區(qū))
  • runnable(正在運行)
  • in Object.wait(處于等待狀態(tài))
  1. java.lang.Thread.State: BLOCKED (on object monitor)
    這段是描述線程狀態(tài),我們知道Java的6種線程狀態(tài)定義在Thread.java中:
//Thread.java
public class Thread implements Runnable {
    ...
    public enum State {
        /**
         * The thread has been created, but has never been started.
         */
        NEW,
        /**
         * The thread may be run.
         */
        RUNNABLE,
        /**
         * The thread is blocked and waiting for a lock.
         */
        BLOCKED,
        /**
         * The thread is waiting.
         */
        WAITING,
        /**
         * The thread is waiting for a specified amount of time.
         */
        TIMED_WAITING,
        /**
         * The thread has been terminated.
         */
        TERMINATED
    }
    ...
}
  1. at xxx 調(diào)用棧
at Test.rightLeft(Test.java:48)
- waiting to lock <0x00000007d56540a0> (a Test$LeftObject)
- locked <0x00000007d5656180> (a Test$RightObject)
at Test$2.run(Test.java:68)
at java.lang.Thread.run(Thread.java:745)

這段線程的調(diào)用棧,可以看到線程在我們執(zhí)行jstack命令的時候運行到Test.java的48行,而在68行到48行之間,線程對一個TestRightObject上了鎖,并且目前在等待TestLeftObject鎖。

4.1.2 jstack分析信息
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007fde73818ab8 (object 0x00000007d56540a0, a Test$LeftObject),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007fde73819f58 (object 0x00000007d5656180, a Test$RightObject),
  which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
    at Test.rightLeft(Test.java:48)
    - waiting to lock <0x00000007d56540a0> (a Test$LeftObject)
    - locked <0x00000007d5656180> (a Test$RightObject)
    at Test$2.run(Test.java:68)
    at java.lang.Thread.run(Thread.java:745)
"Thread-0":
    at Test.leftRight(Test.java:34)
    - waiting to lock <0x00000007d5656180> (a Test$RightObject)
    - locked <0x00000007d56540a0> (a Test$LeftObject)
    at Test$1.run(Test.java:60)
    at java.lang.Thread.run(Thread.java:745)

說明中的信息很詳細,它認為我們的應(yīng)用出現(xiàn)了一個Java層的死鎖,即Thread-1等待一個被Thread-0持有的鎖,Thread-0等待一個被Thread-1持有的鎖,實際上的確也是這樣,最后再來看看源代碼是不是這么回事。

public class Test {
    public static class LeftObject {     
    }
     
    public static class RightObject {
    }
    private Object leftLock = new LeftObject();
    private Object rightLock = new RightObject();
     
    public void leftRight() {
        synchronized (leftLock) {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (rightLock) {
                System.out.println("leftRight");
            }
        }
    }
    public void rightLeft() {
        synchronized (rightLock) {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (leftLock) {
                System.out.println("leftRight");
            }
        }
    }
    public static void main(String[] args) {
        final Test test = new Test();
         
        new Thread(new Runnable() {          
            @Override
            public void run() {
                test.leftRight();
            }
        }).start();
         
        new Thread(new Runnable() {   
            @Override
            public void run() {
                test.rightLeft();
            }
        }).start();
    }
}

4.2 DDMS分析ANR問題

有了上面的基礎(chǔ),再來看看Android的ANR如何分析,Android的DDMS工具其實已經(jīng)給我們提供了一個類似于jstack命令的玩意,可以很好的讓我們調(diào)試的時候?qū)崟r查看Android虛擬機的線程狀況。

4.2.1 使用DDMS—Update Threads工具

使用DDMS的Update Threads工具可以分為如下幾步:
1)選擇需要查看的進程;
2)點擊Update Threads按鈕;
3)在Threads視圖查看該進程的所有線程狀態(tài);


選擇需要查看的進程并點擊更新線程按鈕.png

查看線程狀態(tài).png
4.2.2 閱讀Update Threads的輸出

Update Threads工具可以輸出當前進程的所有線程的狀態(tài),上半部分是線程列表,選中其中一條下半部分將展現(xiàn)出該線程當前的調(diào)用棧。
1. 線程列表
上半部分種的線程列表分為好幾列,其中ID欄表示的序號,其中帶有『*』標志的是守護線程,Tid是線程號,Status表示線程狀態(tài),utime表示執(zhí)行用戶代碼的累計時間,stime表示執(zhí)行系統(tǒng)代碼的累計時間,Name表示的是線程名字。實際上utime還有stime他們具體的含義我也不是太清楚,不過這不影響我們分析問題,這里需要特別注意的是main線程啦,還有線程狀態(tài)。

2. main線程
main線程就是應(yīng)用主線程啦,點擊上半部分線程列表選中main線程,我們可以發(fā)現(xiàn),絕大多數(shù)不操作應(yīng)用的情況下,調(diào)用棧應(yīng)該是如下樣式的:

main線程狀態(tài).png

這是一個空閑等待狀態(tài),等待其他線程或者進程發(fā)送消息到主線程,再由主線程處理相應(yīng)事件。如果主線程在執(zhí)行過程中出現(xiàn)了問題,就會出現(xiàn)ANR,結(jié)合下面關(guān)于線程狀態(tài)的分析可以知道如果主線程的狀態(tài)是MONITOR一般肯定就是出現(xiàn)了ANR了。

3. 線程狀態(tài)
我們剛剛在分心Java線程狀態(tài)時明明只有6個狀態(tài),但是現(xiàn)在Android虛擬機給出的線程狀態(tài)超出了這6個的限制,這也是需要在源碼中尋找答案的,VMThread.java類中有這么一段代碼:

/**
 * Holds a mapping from native Thread statuses to Java one. Required for
 * translating back the result of getStatus().
 */
static final Thread.State[] STATE_MAP = new Thread.State[] {
    Thread.State.TERMINATED,     // ZOMBIE
    Thread.State.RUNNABLE,       // RUNNING
    Thread.State.TIMED_WAITING,  // TIMED_WAIT
    Thread.State.BLOCKED,        // MONITOR
    Thread.State.WAITING,        // WAIT
    Thread.State.NEW,            // INITIALIZING
    Thread.State.NEW,            // STARTING
    Thread.State.RUNNABLE,       // NATIVE
    Thread.State.WAITING,        // VMWAIT
    Thread.State.RUNNABLE        // SUSPENDED
};

而且,native層的Thread.cpp中還有一段代碼:

const char* dvmGetThreadStatusStr(ThreadStatus status) {
    switch (status) {
        case THREAD_ZOMBIE:         return "ZOMBIE";
        case THREAD_RUNNING:        return "RUNNABLE";
        case THREAD_TIMED_WAIT:     return "TIMED_WAIT";
        case THREAD_MONITOR:        return "MONITOR";
        case THREAD_WAIT:           return "WAIT";
        case THREAD_INITIALIZING:   return "INITIALIZING";
        case THREAD_STARTING:       return "STARTING";
        case THREAD_NATIVE:         return "NATIVE";
        case THREAD_VMWAIT:         return "VMWAIT";
        case THREAD_SUSPENDED:      return "SUSPENDED";
        default:                    return "UNKNOWN";
    }
}

由此我們可以看到Android虛擬機中有10種線程狀態(tài),其對應(yīng)關(guān)系如下:

Thread.java中定義的狀態(tài) Thread.cpp中定義的狀態(tài) 說明
TERMINATED ZOMBIE 線程死亡,終止運行
RUNNABLE RUNNING/RUNNABLE 線程可運行或正在運行
TIMED_WAITING TIMED_WAIT 執(zhí)行了帶有超時參數(shù)的wait、sleep或join函數(shù)
BLOCKED MONITOR 線程阻塞,等待獲取對象鎖
WAITING WAIT 執(zhí)行了無超時參數(shù)的wait函數(shù)
NEW INITIALIZING 新建,正在初始化,為其分配資源
NEW STARTING 新建,正在啟動
RUNNABLE NATIVE 正在執(zhí)行JNI本地函數(shù)
WAITING VMWAIT 正在等待VM資源
RUNNABLE SUSPENDED 線程暫停,通常是由于GC或debug被暫停
UNKNOWN 未知狀態(tài)
4.2.3 舉個例子
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button mBtn = (Button) findViewById(R.id.button);
        mBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                print();
            }
        });
    }
    public void print() {
        BufferedReader bufferedReader = null;
        String tmp = null;
        try {
            bufferedReader = new BufferedReader(new FileReader(new File(Environment.getExternalStorageDirectory() + "/test")));
            while ((tmp = bufferedReader.readLine()) != null) {
                Log.i("wangchen", tmp);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

簡單的一個Activity,點擊按鈕時將讀取文件內(nèi)容并進行打印到logcat,本身沒有什么大問題,但是在該Activity的按鈕被點擊時卻出現(xiàn)了未響應(yīng)的情況。
通過DDMS,我們查看到當前未響應(yīng)時主線程一直處于如下調(diào)用狀態(tài):

at android.util.Log.println_native(Native Method) 
at android.util.Log.i(Log.java:173) 
at com.example.wangchen.androitest.MainActivity.print(MainActivity.java:37) 
at com.example.wangchen.androitest.MainActivity$1.onClick(MainActivity.java:26) 
at android.view.View.performClick(View.java:4446) 
at android.view.View$PerformClick.run(View.java:18480) 
at android.os.Handler.handleCallback(Handler.java:733) 
at android.os.Handler.dispatchMessage(Handler.java:95) 
at android.os.Looper.loop(Looper.java:136) 
at android.app.ActivityThread.main(ActivityThread.java:5314) 
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:864) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:680) 
at dalvik.system.NativeStart.main(Native Method)

由上面對主線程的分析可以知道,正常情況下主線程應(yīng)當是處于空閑等待狀態(tài),如果長時間處于處理某一個任務(wù)時就會導致其他被發(fā)送到主線程的事件無法被及時處理,導致ANR,實際上這里的test文件有30M,完全打印是非常耗時的,導致ANR也就理所當然了,所以對于文件讀寫操作還是建議在非主線程操作。

4.3 trace文件分析

在開發(fā)調(diào)試過程中遇到ANR問題大多是可以通過DDMS方法來分析問題原因的,但是所有的ANR問題不一定會在開發(fā)階段出現(xiàn),如果在測試或者發(fā)版之后出現(xiàn)了ANR問題,那么就需要通過traces文件來分析。根據(jù)之前的分析我們知道,traces文件位于/data/anr目錄下,即便是沒有root的手機也是可以通過adb命令將該文件pull出來,一個traces文件中包含了出現(xiàn)ANR時當前系統(tǒng)的所有活動進程的情況,其中每一個進程會包含所有線程的情況,因此文件的內(nèi)容量往往比較大。但是一般造成ANR的進程會被記錄在頭一段,因此盡可能詳細的分析頭一段進程是解析traces文件的重要方法。

4.3.1 trace文件內(nèi)容
adb pull /data/anr/traces.txt ./mytraces.txt

通過命令獲得trace文件,內(nèi)容如下:

// 進程頭部信息:進程的pid號,當前時間,進程名稱
----- pid 4280 at 2016-05-30 00:17:13 -----
Cmd line: com.quicinc.cne.CNEService

// 進程資源狀態(tài)信息
Build fingerprint: 'Xiaomi/virgo/virgo:6.0.1/MMB29M/6.3.21:user/release-keys'
ABI: 'arm'
Build type: optimized
Zygote loaded classes=4124 post zygote classes=18
Intern table: 51434 strong; 17 weak
JNI: CheckJNI is off; globals=286 (plus 277 weak)
Libraries: /system/lib/libandroid.so /system/lib/libcompiler_rt.so /system/lib/libjavacrypto.so /system/lib/libjnigraphics.so /system/lib/libmedia_jni.so /system/lib/libmiuinative.so /system/lib/libsechook.so /system/lib/libwebviewchromium_loader.so libjavacore.so (9)
Heap: 50% free, 16MB/33MB; 33690 objects
Dumping cumulative Gc timings
Total number of allocations 33690
Total bytes allocated 16MB
Total bytes freed 0B
Free memory 16MB
Free memory until GC 16MB
Free memory until OOME 111MB
Total memory 33MB
Max memory 128MB
Zygote space size 1624KB
Total mutator paused time: 0
Total time waiting for GC to complete: 0
Total GC count: 0
Total GC time: 0
Total blocking GC count: 0
Total blocking GC time: 0
suspend all histogram:  Sum: 102us 99% C.I. 3us-25us Avg: 8.500us Max: 25us

// 每條線程的信息
DALVIK THREADS (10):
"Signal Catcher" daemon prio=5 tid=2 Runnable
  | group="system" sCount=0 dsCount=0 obj=0x12c470a0 self=0xaeb8b000
  | sysTid=4319 nice=0 cgrp=default sched=0/0 handle=0xb424f930
  | state=R schedstat=( 111053493 34114006 33 ) utm=6 stm=5 core=0 HZ=100
  | stack=0xb4153000-0xb4155000 stackSize=1014KB
  | held mutexes= "mutator lock"(shared held)
  native: #00 pc 00370e89  /system/lib/libart.so (art::DumpNativeStack(std::__1::basic_ostream<char, std::__1::char_traits<char> >&, int, char const*, art::ArtMethod*, void*)+160)
  native: #01 pc 003504f7  /system/lib/libart.so (art::Thread::Dump(std::__1::basic_ostream<char, std::__1::char_traits<char> >&) const+150)
  native: #02 pc 0035a3fb  /system/lib/libart.so (art::DumpCheckpoint::Run(art::Thread*)+442)
  native: #03 pc 0035afb9  /system/lib/libart.so (art::ThreadList::RunCheckpoint(art::Closure*)+212)
  native: #04 pc 0035b4e7  /system/lib/libart.so (art::ThreadList::Dump(std::__1::basic_ostream<char, std::__1::char_traits<char> >&)+142)
  native: #05 pc 0035bbf7  /system/lib/libart.so (art::ThreadList::DumpForSigQuit(std::__1::basic_ostream<char, std::__1::char_traits<char> >&)+334)
  native: #06 pc 00333d3f  /system/lib/libart.so (art::Runtime::DumpForSigQuit(std::__1::basic_ostream<char, std::__1::char_traits<char> >&)+74)
  native: #07 pc 0033b0a5  /system/lib/libart.so (art::SignalCatcher::HandleSigQuit()+928)
  native: #08 pc 0033b989  /system/lib/libart.so (art::SignalCatcher::Run(void*)+340)
  native: #09 pc 0003f54f  /system/lib/libc.so (__pthread_start(void*)+30)
  native: #10 pc 00019c2f  /system/lib/libc.so (__start_thread+6)
  (no managed stack frames)
"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 obj=0x7541b3c0 self=0xb4cf6500
  | sysTid=4280 nice=-1 cgrp=default sched=0/0 handle=0xb6f5cb34
  | state=S schedstat=( 52155108 81807757 159 ) utm=2 stm=3 core=0 HZ=100
  | stack=0xbe121000-0xbe123000 stackSize=8MB
  | held mutexes=
  native: #00 pc 00040984  /system/lib/libc.so (__epoll_pwait+20)
  native: #01 pc 00019f5b  /system/lib/libc.so (epoll_pwait+26)
  native: #02 pc 00019f69  /system/lib/libc.so (epoll_wait+6)
  native: #03 pc 00012c57  /system/lib/libutils.so (android::Looper::pollInner(int)+102)
  native: #04 pc 00012ed3  /system/lib/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+130)
  native: #05 pc 00082bed  /system/lib/libandroid_runtime.so (android::NativeMessageQueue::pollOnce(_JNIEnv*, _jobject*, int)+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:135)
  at android.app.ActivityThread.main(ActivityThread.java:5435)
  at java.lang.reflect.Method.invoke!(Native method)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:735)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:625)

// ...
----- end 4280 -----
4.3.2 舉個例子

一般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”),線程優(yōu)先級(“prio=5”),線程id(“tid=1”),線程狀態(tài)(Sleeping),比較常見的狀態(tài)還有Native、Waiting;
  • 第4行:分別是線程所處的線程組 (“main”),線程被正常掛起的次處(“sCount=1”),線程因調(diào)試而掛起次數(shù)(”dsCount=0“),當前線程所關(guān)聯(lián)的java線程對象(”obj=0x73f11000“)以及該線程本身的地址(“0xf3c25800”);
  • 第5行:顯示線程調(diào)度信息,分別是該線程在linux系統(tǒng)下得本地線程id (“ sysTid=2957”),線程的調(diào)度有優(yōu)先級(“nice=0”),調(diào)度策略(sched=0/0),優(yōu)先組屬(“cgrp=default”)以及 處理函數(shù)地址(“handle=0xf7770ea0”);
  • 第6行:顯示更多該線程當前上下文,分別是調(diào)度狀態(tài)(從 /proc/[pid]/task/[tid]/schedstat讀出)(“schedstat=( 107710942 40533261 131 )”),以及該線程運行信息 ,它們是線程用戶態(tài)下使用的時間值(單位是jiffies)(“utm=4”), 內(nèi)核態(tài)下得調(diào)度時間值(“stm=6”),以及最后運行該線程的cpu標識(“core=2”);
  • 第7行:表示線程棧的地址(“stack=0xff49d000-0xff49f000”)以及棧大?。ā皊tackSize=8MB”);
  • 后面是線程的調(diào)用棧信息,也是分析ANR的核心所在。
    分析調(diào)試棧發(fā)現(xiàn):at com.sunny.demo.MainActivity.startMethod(MainActivity.java:21) 很容易就可以定位到我們的問題所在。

5 ANR 檢測

5.1 StrictMode

嚴格模式StrictMode是Android SDK提供的一個用來檢測代碼中是否存在違規(guī)操作的工具類,StrictMode主要檢測兩大類問題:
1. 線程策略ThreadPolicy

  • detectCustomSlowCalls: 檢測自定義耗時操作。
  • detectDiskReads: 檢測是否存在磁盤讀取操作。
  • detectDiskWrites: 檢測是否存在磁盤寫入操作。
  • detectNetWork: 檢測是否存在網(wǎng)絡(luò)操作。
    2. 虛擬機策略VmPolicy
  • detectActivityLeaks: 檢測是否存在Activity泄漏。
  • detectLeakedClosableObjects: 檢測是否存在未關(guān)閉的Closable對象泄漏。
  • detectLeakedSqlLiteObjects: 檢測是否存在Sqlite對象泄漏。
  • setClassInstanceLimit: 檢測類實例個數(shù)是否超過限制。

可以看到,其中ThreadPolicy可以用來檢測可能存在的主線程耗時操作,解決這些檢測到問題能搞減少應(yīng)用發(fā)生ANR的概率。注意:只能在Debug模式下使用它。在應(yīng)用初始化的地方,例如Application或者MainActivity類的onCreate方法中執(zhí)行如下代碼即可:

@Override
protected void onCreate(Bundle savedInstanceState) {
  if (BuildConfig.DEBUG) {
    //開啟線程模式所有檢測策略
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build());
    //開啟虛擬機模式所有檢測策略
    StrictMode.setVmPolicy(new VmPolicy.Builder().detectAll().penaltyLog().build());
  }
  super.onCreate(savedInstanceState);
}

上面的初始化代碼調(diào)用penaltyLog表示在Logcat中打印日志,調(diào)用detectAll方法表示啟動所有的檢測策略,可以根據(jù)需求只開啟部分檢測功能。

5.2 BlockCanary

BlockCanary是一個非入侵式的性能監(jiān)控函數(shù)庫,它的用法和LeakCanary類似,用來監(jiān)控應(yīng)用主線程卡頓。它的基本原理是利用主線程的消息隊列處理機制,通過對比消息分發(fā)開始和結(jié)束時間點來判斷是否超過設(shè)定時間。
在build.gradle中添加依賴

dependencies {
  compile 'com.github.moduth:blockcanary-android:1.2.1'

  //僅在debug包啟用BlockCanary進行卡頓監(jiān)控和提示
  debugCompile 'com.github.moduth:blockcanary-android:1.2.1'
  releaseCompile 'com.github.moduth:blockcanary-no-op:1.2.1'
}
public class DemoApplication extends Application {
  @Override
  public void onCreate() {
    //在主進程初始化調(diào)用
    BlockCanary.install(this, new AppBlockCanaryContext()).start();
  }
}

public class AppBlockCanaryContext extends BlockCanaryContext {
  //實現(xiàn)各種上下文,包括應(yīng)用標識符,用戶uid,網(wǎng)絡(luò)類型,卡慢判斷閾值,Log保存位置等
}

參考鏈接:

[1] 安卓應(yīng)用無響應(yīng),你真的了解嗎?
[2] Android ANR的產(chǎn)生與分析
[3] Android系統(tǒng)架構(gòu) —— IMS的ANR 產(chǎn)生流程 ★★
[4] 理解Android ANR的信息收集過程
[5] Android ANR:原理分析及解決辦法
[6] Android應(yīng)用ANR分析
[7] ANR產(chǎn)生的原因及其定位分析
[8] ANR產(chǎn)生的原因及定位分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容