Android ANR那些事

从技术的角度看,ANR对用户体验的伤害仅次于Crash。多年的开发实践过程中遇到过不少ANR问题,有两次印象比较深,都是App冷启动后卡死在广告页进不了主页导致应用市场审核不通过。
在发版审核这种关键节点,分析原因解决问题的速度很重要,而速度取决于分析问题的思路,相关技术的日常总结以及对业务和代码的熟悉度。下面先从一个实际例子开始。

1 实际案例

这是一次ANR后,分析trace文件得到的线程等待关系图


ANR-线程关系图.png

图中UI线程(tid=1)和另外两个Rx调度线程(tid=55,tid=96)都在等待锁 <0x33199586> *Provider,锁<0x33199586> *Provider被另一个Rx调度线程(tid=49)持有,线程49持有锁<0x33199586> *Provider后,又调用了一个同步方法持有了锁<0x14818fd3> Object(被线程84等待),这个同步方法内部在执行网络请求导致UI线程长时间Block进而造成了ANR。

UI线程参与并发很容易造成ANR, 并发一般涉及到锁,如果UI线程等待锁的时间过长都会发生ANR。图中如果不是线程49,而是线程55或线程96先持有了锁<0x33199586> *Provider并去执行一段很耗时代码也会造成ANR。

UI线程长时间Block会造成ANR, 网络线程长时间Block有时也会造成严重的问题。开发中遇到过锁竞争造成网络线程Block导致进首页后长时间loading的case,Block的原因是发送请求时在拦截器里需要添加一个公参,获取公参的方法有锁相关的逻辑。图中线程55和线程96都是网络线程,即使UI线程没有参与锁竞争造成ANR,这两个网络线程也会因为Block造成端到端(从客户端发出请求到客户端收到响应)时间过长。

定位到原因后问题就迎刃而解了,UI线程使用不会Block的api,同时消除网络线程之间的锁竞争就可以快速解决这个问题。

2 分析方法

至今还记得刚入行第一次遇到ANR时自己完全没有思路的情形。不知道看哪些日志,面对海量日志不知道怎么快速分析提取有效信息,更不清楚系统检测ANR的机制。只是大概知道UI线程执行耗时操作超过一定阈值时会被系统检测到,然后弹FC对话框。这段重点从实操的角度说下分析ANR的方法。

2.1 分析Trace文件

Trace文件记录了发生ANR时的进程和线程信息,分析过程主要分3步

  • 找到trace文件并导出
    trace文件一般都在/data/anr/目录下,trace文件名有的不带包名,有的带包名,有的既带包名又带日期
    系统存储ANR文件的位置可以通过 adb shell getprop 得到,[dalvik.vm.stack-trace-file]:[/data/anr/traces.txt]
shell@PD1415BA:/data/anr $ ls -al
-rw-rw-rw- system   system          0 2018-09-12 15:20 packages-fail.txt
-rw-rw-rw- system   system     615759 2017-02-16 15:47 traces_com.*.*.txt
-rw-rw-rw- system   system     642303 2018-09-11 16:12 traces_com.*.*.txt
-rw-rw-rw- system   system     475820 2018-02-01 19:44 traces_com.omottec.demoapp.txt

系统还有个存储ANR历史文件的地方

shell@PD1415BA:/data/system/dropbox $ ls -al
-rw------- system   system        316 2018-09-10 03:31 SYSTEM_BOOT@1536521506856.txt
-rw-r--r-- system   system      49681 2018-09-11 16:13 data_app_anr@1536653591215.txt.gz
  • 找到发生ANR的进程和UI线程
    Trace文件里的第一个进程一般就是发生ANR的进程(和系统写ANR日志文件的逻辑有关),进程下的第一个线程一般就是UI线程,UI线程更准确的判断方法是看sysTid和pid是否相等,相等的就是UI线程,tid是创建线程时的序号。
----- pid 13178 at 2018-05-03 00:54:42 -----
Cmd line: com.*.*
...
DALVIK THREADS (97):
// "线程名" prio=优先级 tid=线程创建序号 线程Native层的状态
"main" prio=5 tid=1 Blocked
    // group=线程组名 sCount=挂起次数(suspend count) dsCount=调试被挂起次数(debug suspend count) obj=线程的Java对象地址 self=线程Native对象地址
  | group="main" sCount=1 dsCount=0 obj=0x74240c30 self=0xf5027800
  // sysTid=内核线程id nice=调度优先级 cgrp=调度属组 sched=线程调度策略/优先级 handle=线程处理函数地址  
  | sysTid=13178 nice=0 cgrp=default sched=0/0 handle=0xf7753bec
  // state=调度状态 schedstat=(线程执行时间 线程等待时间 线程执行的时间片长度) utm=用户态时间 stm=内核态时间 core=最后执行线程的cup核序号
  | state=S schedstat=( 5541520948 4506511097 13597 ) utm=430 stm=124 core=2 HZ=100
  // stack=线程栈地址范围 stackSize=线程栈大小
  | stack=0xff122000-0xff124000 stackSize=8MB
  | held mutexes=
  at com.*.*.*Provider.query(*Provider.java:-1)
  - waiting to lock <0x33199586> (a com.*.uuid.*Provider) held by thread 49
  at android.content.ContentProvider.query(ContentProvider.java:1010)
  at android.content.ContentProvider$Transport.query(ContentProvider.java:214)
  at android.content.ContentResolver.query(ContentResolver.java:504)
  at android.content.ContentResolver.query(ContentResolver.java:439)
  at com.*.*(*.java:96)
  at com.*.*(*.java:84)
  at com.*.*.utils.ag.b(*.java:38)
  at com.*.*.*Activity.n(*Activity.java:81)
  at com.*.*.*Activity.onCreate(*Activity.java:64)
  at android.app.Activity.performCreate(Activity.java:6049)
  at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1106)
  at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2304)
  at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2413)
  at android.app.ActivityThread.access$800(ActivityThread.java:154)
  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1317)
  at android.os.Handler.dispatchMessage(Handler.java:102)
  at android.os.Looper.loop(Looper.java:135)
  at android.app.ActivityThread.main(ActivityThread.java:5308)
  at java.lang.reflect.Method.invoke!(Native method)
  at java.lang.reflect.Method.invoke(Method.java:372)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:913)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:708)

直接搜索main也可以快速定位UI线程, 搜索的结果可能有很多处,因为很多线程所属线程组的name为main,要注意和name为main的UI线程区分开。

  • 分析UI线程和相关线程的状态
    trace文件里的线程状态和Java里面java.lang.Thread.State枚举表示的线程状态不是一个概念,有个映射关系。trace文件里的线程状态是native层的线程状态,理解线程状态可以帮助更好的分析问题,映射关系如下,在Android源码里有定义。
class VMThread
{
        ...
    /**
     * Holds a mapping from native Thread statii 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
    };
    ...
}

https://android.googlesource.com/platform/dalvik/+/eclair-release/vm/Thread.h

/*
 * Current status; these map to JDWP constants, so don't rearrange them.
 * (If you do alter this, update the strings in dvmDumpThread and the
 * conversion table in VMThread.java.)
 *
 * Note that "suspended" is orthogonal to these values (so says JDWP).
 */
typedef enum ThreadStatus {
    /* these match up with JDWP values */
    THREAD_ZOMBIE       = 0,        /* TERMINATED */
    THREAD_RUNNING      = 1,        /* RUNNABLE or running now */
    THREAD_TIMED_WAIT   = 2,        /* TIMED_WAITING in Object.wait() */
    THREAD_MONITOR      = 3,        /* BLOCKED on a monitor */
    THREAD_WAIT         = 4,        /* WAITING in Object.wait() */
    /* non-JDWP states */
    THREAD_INITIALIZING = 5,        /* allocated, not yet running */
    THREAD_STARTING     = 6,        /* started, not yet on thread list */
    THREAD_NATIVE       = 7,        /* off in a JNI native method */
    THREAD_VMWAIT       = 8,        /* waiting on a VM resource */
} ThreadStatus;

/*
 * Thread suspend/resume, used by the GC and debugger.
 */
typedef enum SuspendCause {
    SUSPEND_NOT = 0,
    SUSPEND_FOR_GC,
    SUSPEND_FOR_DEBUG,
    SUSPEND_FOR_DEBUG_EVENT,
    SUSPEND_FOR_STACK_DUMP,
    SUSPEND_FOR_DEX_OPT,
#if defined(WITH_JIT)
    SUSPEND_FOR_JIT,
#endif
} SuspendCause;

上面案例里面线程1,55,96,84都处于Blocked状态, 在等待monitor锁,线程49处于Native状态,在执行一个Native方法,具体调用栈如下

"RxIoScheduler-3" daemon prio=5 tid=49 Native
  | group="main" sCount=1 dsCount=0 obj=0x32c0aa60 self=0xf5226c00
  | sysTid=13308 nice=0 cgrp=default sched=0/0 handle=0xf5313a80
  | state=S schedstat=( 1354206596 302762808 4264 ) utm=111 stm=24 core=2 HZ=100
  | stack=0xde4b3000-0xde4b5000 stackSize=1036KB
  | held mutexes=
  kernel: (couldn't read /proc/self/task/13308/stack)
  native: #00 pc 0003838c  /system/lib/libc.so (read+8)
  native: #01 pc 000317f5  /system/lib/libc.so (__sread+8)
  native: #02 pc 0003153f  /system/lib/libc.so (__srefill+206)
  native: #03 pc 000501f3  /system/lib/libc.so (fread+162)
  native: #04 pc 0001d431  /system/lib/libc.so (android_getaddrinfofornet+1364)
  native: #05 pc 00000c3f  /system/vendor/lib/libNimsWrap.so (android_getaddrinfofornet+38)
  native: #06 pc 0002098f  /system/lib/libjavacore.so (???)
  native: #07 pc 0009ba6b  /data/dalvik-cache/arm/system@framework@boot.oat (Java_libcore_io_Posix_android_1getaddrinfo__Ljava_lang_String_2Landroid_system_StructAddrinfo_2I+134)
  at libcore.io.Posix.android_getaddrinfo(Native method)
  at libcore.io.ForwardingOs.android_getaddrinfo(ForwardingOs.java:55)
  at java.net.InetAddress.lookupHostByName(InetAddress.java:438)
  at java.net.InetAddress.getAllByNameImpl(InetAddress.java:252)
  at java.net.InetAddress.getAllByName(InetAddress.java:215)
  at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:142)
  at org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:169)
  at org.apache.http.impl.conn.AbstractPooledConnAdapter.open(AbstractPooledConnAdapter.java:124)
  at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:365)
  at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:560)
  at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:658)
  at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:632)
  at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:621)
  at com.*.*(*.java:592)
  - locked <0x14818fd3> (a java.lang.Object)
  at com.*.*.*Provider.query(*Provider.java:149)
  - locked <0x33199586> (a com.*.*.*Provider)
  at android.content.ContentProvider.query(ContentProvider.java:1010)
  at android.content.ContentProvider$Transport.query(ContentProvider.java:214)
  at android.content.ContentResolver.query(ContentResolver.java:504)
  at android.content.ContentResolver.query(ContentResolver.java:439)
  at com.*.*(*.java:696)
  at com.*.*(*.java:183)
  at com.*.*(*.java:162)
  at com.*.*.utils.ag$1.a(*.java:30)
  at com.*.*.utils.ag$1.call(*.java:26)
  at rx.c.a(Observable.java:8666)
  at rx.internal.operators.ac$1.call(OperatorSubscribeOn.java:94)
  at rx.internal.schedulers.a$b$1.call(CachedThreadScheduler.java:220)
  at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55)
  at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:422)
  at java.util.concurrent.FutureTask.run(FutureTask.java:237)
  at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:152)
  at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:265)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
  at java.lang.Thread.run(Thread.java:818)

如果ANR的原因是UI线程参与锁竞争,梳理一张线程之间的关系图可以让原因一目了然。

2.2 分析Logcat日志

发生ANR时logcat会打印很多信息,下面ANR in指向的Activity和trace日志中UI线程发生Block时的Activity一致, Reason给出了输入事件分发超时的原因。

// 发生ANR的Activity
05-03 00:54:46.888 2479 2504 E ActivityManager: ANR in com.*.*(com.*.*/.*.*Activity)
05-03 00:54:46.888 2479 2504 E ActivityManager: PID: 13178
// 有应用但没有焦点窗口来处理输入事件
05-03 00:54:46.888 2479 2504 E ActivityManager: Reason: Input dispatching timed out (Waiting because no window has focus but there is a focused application that may eventually add a window when it finishes starting up.)
// 最近1分钟/5分钟/15分钟的CPU平均负载
05-03 00:54:46.888 2479 2504 E ActivityManager: Load: 8.41 / 7.82 / 7.5
// ANR发生前各进程的CPU使用率,每个进程的使用率又分为用户态和内核态
05-03 00:54:46.888 2479 2504 E ActivityManager: CPU usage from 897ms to -4816ms ago:
05-03 00:54:46.888 2479 2504 E ActivityManager: 70% 2479/system_server: 37% user + 33% kernel / faults: 12344 minor
// 发生ANR进程的CPU使用率 / 缺页错误次数
05-03 00:54:46.888 2479 2504 E ActivityManager: 66% 13178/com.*.*: 50% user + 16% kernel / faults: 30732 minor 11 major
...
// +表示进程是在ANR发生前后的两个采样时间段内新建的
05-03 00:54:46.888 2479 2504 E ActivityManager: +0% 13871/screencap: 0% user + 0% kernel
05-03 00:54:46.888 2479 2504 E ActivityManager: 0% TOTAL: 0.1% user + 0% kernel + 0% irq + 0% softirq
// ANR发生后各进程的CPU使用率
05-03 00:54:46.888 2479 2504 E ActivityManager: CPU usage from 4268ms to 4791ms later:
05-03 00:54:46.888 2479 2504 E ActivityManager: 23% 13178/com.*.*: 17% user + 5.7% kernel / faults: 821 minor
...

某一时间点的CPU负载可以理解为正在使用和等待使用CPU的活动进程数,对一段时间内的多个时间点采样可以得到这一段时间的平均负载,由于ANR的超时限制基本都在1分钟以内,所以最近1分钟的平均负载参考价值更大,也可以通过 /proc/loadavg 文件查看CPU负载。如果CPU负载过高导致当前进程的UI线程分配不到充足的CPU时间片也会导致ANR。

logcat一般会打印ANR发生前后两段时间的CPU使用率,有时候是ANR发生之后的两段时间,也可以通过/proc/pid/stat, /proc/pid/task/tid/stat 文件查看CPU使用率。如果当前进程的其他线程或其他进程的CPU使用率过高导致当前进程的UI线程分配不到充足的CPU时间片也会导致ANR。

faults: *** minor *** major
minor指Minor Page Fault(次要页错误)和major指Major Page Fault(主要页错误),内核读数据时会先后查找CPU的高速缓存和物理内存,如果如果找不到会发出一个MPF信息,请求将数据加载到内存,数据被加载到内存后,内核再次读取时,会发出一个MnPF信息。所以minor可以理解为进程在访问内存,major可以理解为进程在做磁盘io,分析问题需要注意MPF过多,iowait CPU占用过高的信息。

3 系统检测和报告ANR的机制

3.1 检测机制

3.1.1 InputEvent超时检测

linux底层识别到按键,触屏这些物理输入时会把他们抽象转换成Android系统可以处理的输入事件。system_server进程的InputDispatcherThread线程中InputDispatcher负责分发输入事件,等待分发的输入事件都会进入outboundQueue排队,InputDispatcher分发事件时会寻找接收输入事件的焦点窗口并判断窗口是否准备好处理更多的输入事件,如果窗口可以处理就会通过IPC机制(InputChanel)把输入事件发送给应用进程的窗口,发送成功后输入事件会从outboundQueue转移到waitQueue,窗口一般会把接收到的输入事件传递给View树处理并通过IPC机制通知InputDispatcher把成功分发的事件从waitQueue中移除,开发分发新事件。
如果没有焦点窗口或窗口无法处理更多输入事件导致等待超时,会触发AMS报告ANR并丢弃当前事件开始分发其他事件。


ANR-InputEvent检测.png
int32_t InputDispatcher::findFocusedWindowTargetsLocked(nsecs_t currentTime,
        const EventEntry* entry, Vector<InputTarget>& inputTargets, nsecs_t* nextWakeupTime) {
    int32_t injectionResult;
    std::string reason;
    // If there is no currently focused window and no focused application
    // then drop the event.
    if (mFocusedWindowHandle == NULL) {
        if (mFocusedApplicationHandle != NULL) {
            injectionResult = handleTargetsNotReadyLocked(currentTime, entry,
                    mFocusedApplicationHandle, NULL, nextWakeupTime,
                    "Waiting because no window has focus but there is a "
                    "focused application that may eventually add a window "
                    "when it finishes starting up.");
            goto Unresponsive;
        }
        ALOGI("Dropping event because there is no focused window or focused application.");
        injectionResult = INPUT_EVENT_INJECTION_FAILED;
        goto Failed;
    }
    ...
    // Check whether the window is ready for more input.
    reason = checkWindowReadyForMoreInputLocked(currentTime,
            mFocusedWindowHandle, entry, "focused");
        ...
}

案例中InputDispatcher在寻找焦点应用和窗口时发现mFocusedWindowHandle == NULL && mFocusedApplicationHandle != NULL,所以打印了下面的日志。
Reason: Input dispatching timed out (Waiting because no window has focus but there is a focused application that may eventually add a window when it finishes starting up.)

3.1.2 Service超时检测

AMS启动Service时会给system_server进程的ActivityManager线程所绑定的MessageQueue中发送一个用于检测超时的消息。如果应用进程的UI线程上Service的生命周期方法在限定的时间内执行完,会调用AMS的serviceDoneExecuting方法移除这个消息,使AMS不再处理这个消息。如果限定时间内,Service的生命周期方法未执行完,AMS就会处理这个消息,进而报告ANR


ANR-Service检测.png

3.1.3 Broadcast超时检测

Broadcast超时检测的机制和Service类似也是利用了消息机制,广播分为并行广播和串行广播,只有串行广播会发生ANR。systerm_server进程中的BroadcastQueue每次分发广播时会检测广播总的处理时间是否超过了2×time×num,time是单个接收器处理广播的超时时间,num是接收器的数量。如果超过了会进入一个超时判断,如果未超过会开始分发广播,分发广播给第一个接收器时会给AMS线程发送一个定时消息BROADCAST_TIMEOUT_MSG,
每次处理定时消息时也会进入一个超时判断,如果发生了超时则会丢弃当前广播,报告ANR,并开始分发新广播。定时消息只有在串行广播被所有接收器处理完后才会移除。


ANR-Broadcast检测.png

判断超时的逻辑比较巧妙

final void broadcastTimeoutLocked(boolean fromMsg) {
        ...
        long now = SystemClock.uptimeMillis();
        BroadcastRecord r = mOrderedBroadcasts.get(0);
        if (fromMsg) {
            ...
             // r.receiverTime是 当前接收器 接收广播的时间,这个值会动态更新
             // mTimeoutPeriod是单个广播的超时时间
            long timeoutTime = r.receiverTime + mTimeoutPeriod;
            if (timeoutTime > now) {
                // We can observe premature timeouts because we do not cancel and reset the
                // broadcast timeout message after each receiver finishes.  Instead, we set up
                // an initial timeout then kick it down the road a little further as needed
                // when it expires.
                if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST,
                        "Premature timeout ["
                        + mQueueName + "] @ " + now + ": resetting BROADCAST_TIMEOUT_MSG for "
                        + timeoutTime);
                setBroadcastTimeoutLocked(timeoutTime);
                return;
            }
        }
        ...
        r.receiverTime = now;
        ...
        // Move on to the next receiver.
        finishReceiverLocked(r, r.resultCode, r.resultData,
                r.resultExtras, r.resultAbort, false);
        scheduleBroadcastsLocked();
        if (!debugging && anrMessage != null) {
            // Post the ANR to the handler since we do not want to process ANRs while
            // potentially holding our lock.
            mHandler.post(new AppNotResponding(app, anrMessage));
        }
    }

假设一条串行广播有2个接收器,在时间点t1给第一个接收器发送了广播,则r.receiverTime的初始值为t1, AMS线程在t1+x(x为超时时间)的时间点会处理这个消息,如果这段时间内第一个接收器处理完了广播并通知了BroadcastQueue广播已处理,BroadcastQueue会在时间点t2给第二个接收器发送广播,此时r.receiverTime=t2,这样在第一次处理超时消息时 timeoutTime = r.receiverTime + mTimeoutPeriod = t2 + x > now = t1 + x,这样就不会走下面分发新广播和报告ANR的逻辑了,并且会再发送一个超时消息,在时间点t2+x检测第二个接收器是否处理完了广播,如果在t2到t2+x期间第二个接收器处理完了广播,则会移除t2+x时间点的超时消息,不再检测ANR, 并开始下一个串行广播的分发。


ANR-广播检测定时消息.png

三种检测机制中InputEvent的检测利用了Native层的超时机制,Service, Broadcast的检测利用了Java层的消息机制。这里用于检测超时的消息机制运行在system_server进程中,注意和应用进程中处理应用逻辑的消息机制区分开。系统检测ANR的机制侧重于讲解整体流程和基本原理,更多细节可以参考相关链接里的源码分析。

3.2 报告机制

无论是哪种类型的ANR,最终都会调AppErrors的appNotResponding方法,方法内部的主要逻辑就是写trace文件,打印logcat日志,写dropbox文件,弹ANR对话框。

/**
 * ProcessRecord app:发生ANR的进程
 * ActivityRecord activity:发生ANR的Activity
 * String annotation:发生ANR的原因
 */
final void appNotResponding(ProcessRecord app, ActivityRecord activity,
            ActivityRecord parent, boolean aboveSystem, final String annotation) {
        ...
        long anrTime = SystemClock.uptimeMillis();
        if (ActivityManagerService.MONITOR_CPU_USAGE) {
            mService.updateCpuStatsNow();
        }
        ...
        // Log the ANR to the main log.
        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");
        }
        ProcessCpuTracker processCpuTracker = new ProcessCpuTracker(true);
        ...
        // For background ANRs, don't pass the ProcessCpuTracker to
        // avoid spending 1/2 second collecting stats to rank lastPids.
        File tracesFile = ActivityManagerService.dumpStackTraces(
                true, firstPids,
                (isSilentANR) ? null : processCpuTracker,
                (isSilentANR) ? null : lastPids,
                nativePids);
        String cpuInfo = null;
        if (ActivityManagerService.MONITOR_CPU_USAGE) {
            mService.updateCpuStatsNow();
            synchronized (mService.mProcessCpuTracker) {
                cpuInfo = mService.mProcessCpuTracker.printCurrentState(anrTime);
            }
            info.append(processCpuTracker.printCurrentLoad());
            info.append(cpuInfo);
        }
        info.append(processCpuTracker.printCurrentState(anrTime));
        Slog.e(TAG, info.toString());
        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);
        }
        ...
        mService.addErrorToDropBox("anr", app, app.processName, activity, parent, annotation,
                cpuInfo, tracesFile, null);
        ...
        synchronized (mService) {
            ...
            // Bring up the infamous App Not Responding dialog
            Message msg = Message.obtain();
            msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;
            msg.obj = new AppNotRespondingDialog.Data(app, activity, aboveSystem);
            mService.mUiHandler.sendMessage(msg);
        }
    }

AMS的dumpStackTraces方法会写trace文件,文件格式一般如下
----- pid 发生ANR的进程号 at Date -----
Cmd line: 发生ANR的进程名
内存和GC信息
DALVIK THREADS (线程总数):
UI线程的调用栈
其他线程的调用栈
----- end 发生ANR的进程号 -----
----- pid 其他进程的进程号 at Date -----
...
----- end 其他进程的进程号 -----
知道日志格式后分析动辄上万行的ANR日志时心里也不会慌

4 避坑挖坑经验

了解了系统检测ANR的机制后,在日常开发中要注意两点:

  • 不要在UI线程上写可能导致超时的逻辑
    • 耗时操作异步化,可以利用AsyncTask, HandlerThread,AsyncQueryHandler,CursorLoader, IntentService, 注册广播时指定处理线程, RxJava及基于RxJava的扩展库等实现异步化
    • 注意UI线程参与锁竞争
    • 注意UI性能的优化
    • 注意死循环
    • 注意死锁
  • 非UI线程上如果有太耗CPU的操作要考虑对UI线程CPU使用率的影响

如果是做SDK的同学还应该多注意两点

  • 尽量少写同步逻辑,如果必须写,尽量缩小同步的范围
  • public api尽量加上UiThread,WorkerThread,AnyThread这些Android支持库的注解,防止业务同学误用

开发中遇到过App冷启动时十几个库的初始化和多处业务代码都调用一个底层库的api来获取一个通用数据,这个api的内部有多处锁的逻辑导致启动时发生了ANR。对于这种复杂case既需要考虑api内部的设计,也要考虑业务方对api的使用场景,最后的解决方案是业务方建立缓存,开一个单线程调可能导致block的api去获取数据和写缓存,其他线程调不会导致block的方法去读缓存,读不到时给单线程提交获取数据的任务(梳理各种库和业务逻辑后发现第一次可以取不到),缓存的读写逻辑用volatile保持可见性即可,不必使用比较重的锁逻辑。

5 自动化监控上报分析

除了日常开发中践行良好的代码实践保证开发质量,应用上线后我们还需要一套自动化的机制来监控上报分析每天线上发生的ANR。


ANR-自动化监控方案.png

5.1 监听ANR及UI线程Block

5.1.1 监听ANR

主要原理是利用了系统报告ANR的机制和Java的FileObserver机制监听trace文件,如果监听到trace文件有写操作则说明发生了ANR。

5.1.2 监听UI线程Block

这里介绍两种监听UI线程Block的方式

  • 利用消息机制监听UI线程是否有发生Block, 可以创建一个watch线程,在时间点t给一个int型变量tick赋值x即tick=x, 同时给ui线程post一个runnable, runnable的作用是对tick进行自增操作,然后watch线程sleep 5s(值可以调整), 5s后读取tick的值,如果tick=x则说明5s内ui线程都没有完成自增操作,发生了Block,如果tick=x+1则ui线程没有发生block, 10s后可以再次读取tick的值


    ANR-UI线程Block检测.png
  • 另一种利用消息机制的方式原理上更加简单。ui线程的looper在分发消息前后都会打印日志,给ui线程的looper设置一个Printer后,通过计算打印日志的时间差得到处理消息的时间,进而判断是否发生了Block。

public static void loop() {
        ...
        for (;;) {
            ...
            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
            ...
            try {
                msg.target.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            ...
            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
            ...
        }
    }

5.2 收集信息

发生ANR时需要收集两种信息,trace文件和logcat日志

可以利用FileObserver来监听trace文件,当发生ANR并生成trace文件后可以在FileObserver的回调中做文件的解析和上报。上报日志的粒度需要权衡,一般的ANR问题,拿到发生ANR进程的线程栈信息就可以定位原因,但对于一些复杂诡异ANR,如果能拿到其他进程的信息分析时会多一些线索,拿到的现场日志越多,分析就越接近客观和准确。但上传更多的日志要考虑对传输和存储的影响。

logcat日志可以通过List<ProcessErrorStateInfo> ActivityManager#getProcessesInErrorState获取发生ANR时的Logcat日志,
android.app.ActivityManager.ProcessErrorStateInfo#shortMsg: Logcat日志中的Reason,
android.app.ActivityManager.ProcessErrorStateInfo#longMsg: Logcat日志,
Logcat里CPU相关的日志也可用通过读取cpu相关的文件获取。

5.3 初步分析

对于客户端上报的ANR日志,服务端可以做初步的聚类分析,合并同类问题,降低人工筛选归类的成本。分析时可以利用trace文件和logcat日志的格式。

5.4 分配工单

初步分析后可以创建工单,根据一定的匹配规则把问题分配给对应的RD进行进一步的处理。

相关链接
https://developer.android.com/training/articles/perf-anr?hl=zh-cn#java
https://stackoverflow.com/questions/704311/android-how-do-i-investigate-an-anr
https://rayleeya.iteye.com/blog/1955657
http://duanqz.github.io/2015-10-12-ANR-Analysis
https://www.jianshu.com/p/30c1a5ad63a3
https://www.jianshu.com/p/ad1a84b6ec69
https://github.com/markzhai/AndroidPerformanceMonitor

推荐阅读更多精彩内容