前言

ANR 即 Applicatipon No Response,程序无呼应。Android 体系规划了 ANR 机制,其目的是监控与其交互的组件(Activity 等)和用户交互(InputEvent)的超时状况。这样能够判别使用进程(主线程)是否存在卡死或呼应过慢的问题

相比 Crash,ANR 问题存在原因复杂,不易定位的特色,本文主要包含以下内容

  1. ANR 作业流程
  2. 怎么监控 ANR?
  3. 怎么定位 ANR 原因?

ANR 作业流程

ANR 或许触发的机遇有多种,通常能够分为以下几方面:

【 Android 性能优化】ANR 问题如何监控?

图片来源:今天头条 ANR 优化实践系列 – 规划原理及影响要素

其基本原理其实 WatchDog 的思维,假如宣布的事件,在必定时刻内没有消费,则触发 ANR。详细源码就不在这儿跟了,想详细了解的同学可查看:微信Android客户端的ANR监控计划

这儿说一下整体流程,如下图所示:

【 Android 性能优化】ANR 问题如何监控?

  1. 产生 ANR 后,体系会搜集许多进程数据,进行仓库转储,以生成 ANR Trace文件。其间,第一个被搜集的进程必定是产生 ANR 的进程。
  2. 体系会向这些使用进程发送 SIGQUIT 信号,这些使用进程收到信号后开端进行仓库转储
  3. 使用进程 Dump 仓库成功后经过 Socket 与体系进程通讯写 Trace 文件
  4. 在 Trace 文件写入完成后,假如产生 ANR 的进程是前台进程则弹出 Dialog,不然则直接杀死进程

怎么监控 ANR?

在了解了 ANR 的作业流程之后,咱们该怎么监控 ANR 的产生呢?

ANR WatchDog 检测思路

已然 ANR 的原因是输入在定时刻内没有呼应,那么咱们很自然地想到,向主线程发送一个使命,假如一段时刻内没有被履行的话,就认为产生了 ANR

这个思路主要有以下几个问题

  1. 不准确,超时条件纷歧定会导致 ANR,例如,5 秒超时只是在 TouchEvent 未被消耗时产生 ANR 的条件之一,而其他条件则纷歧定是 5 秒。
  2. 漏检测:假如超时时刻定为 5 秒,去检测 TouchEvent 的 ANR 存在必定的漏检测的概率(周期不同步)。

ANR 信号监听思路

在上面介绍 ANR 整体流程时,咱们注意到当 ANR 产生时会发送 SIGQUIT 信号,那么咱们经过监听这一信号不就能够完成 ANR 监控了吗?事实上 XCrash 与 Matrix 都是经过这种办法完成 ANR 监控的

在这儿需求注意,默许状况下进程经过SignalCatcher监听SIGQUIT信号,进行仓库转储生成 ANR Trace 文件。因而当咱们监听SIGQUIT信号后,需求重新向SignalCatcher发送SIGQUIT

假如缺少重新向 SignalCatcher 发送 SIGQUIT 信号的过程,Android System 办理服务(AMS)将一直等候 ANR 进程写入仓库信息。直到超过20秒的超时时刻,AMS 才会被迫中断,并持续后续流程。这将导致 ANR 弹窗的显现十分缓慢(因为超时时刻为20秒),一起在 /data/anr 目录下也无法生成完好的 ANR Trace 文件。

误报状况处理

当监听到 SIGQUIT 信号时,纷歧定是产生了 ANR。

Matrix 的文档中提到了两种误报的状况:

  1. 比方或许是其它进程 ANR 了,产生 ANR 的进程不是仅有需求进行仓库转储的进程。体系会搜集许多其他进程进行仓库转储,用于生成 ANR Trace 文件
  2. 厂商或者是开发者自己发送的SIGQUIT信号,发送SIGQUIT信号其实是很容易的一件工作

因而咱们需求在监听到信号时再进行一次检查:在 ANR 弹窗前,会给产生 ANR 的进程符号一个 NOT_RESPONDING 的 flag,而这个 flag 咱们能够经过 ActivityManager 来获取

private static boolean checkErrorState() {
    try {
        Application application = sApplication == null ? Matrix.with().getApplication() : sApplication;
        ActivityManager am = (ActivityManager) application.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.ProcessErrorStateInfo> procs = am.getProcessesInErrorState();
        if (procs == null) return false;
        for (ActivityManager.ProcessErrorStateInfo proc : procs) {
            if (proc.pid != android.os.Process.myPid()) continue;
            if (proc.condition != ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) continue;
            return true;
        }
        return false;
    } catch (Throwable t){
        MatrixLog.e(TAG,"[checkErrorState] error : %s", t.getMessage());
    }
    return false;
}

如上所示,咱们能够在监听到信号时判别当时进程是否被符号为 NOT_RESPONDING 来判别当时进程是否产生了 ANR

漏报状况处理

当进程被符号为 NOT_RESPONDING 时必定产生了 ANR,可是当进程产生了 ANR 时,纷歧定会被符号为 NOT_RESPONDING

Matrix 的文档中提到了两种漏报状况

  1. 后台ANR(SilentAnr): 后台 ANR 会直接杀死进程,不会走到符号状况的代码
  2. 厂商定制逻辑: 相当一部分机型(比方 OPPO、VIVO 两家的高版别 Android )修改了 ANR 的逻辑,即使是前台 ANR 也会直接杀死进程

Matrix 经过判别主线程在收到 SIGQUIT 信号时是否处于卡顿状况来判别当时是否产生 ANR,如下所示

private static boolean isMainThreadStuck(){
    try {
        MessageQueue mainQueue = Looper.getMainLooper().getQueue();
        Field field = mainQueue.getClass().getDeclaredField("mMessages");
        field.setAccessible(true);
        final Message mMessage = (Message) field.get(mainQueue);
        if (mMessage != null) {
            long when = mMessage.getWhen();
            if(when == 0) {
                return false;
            }
            long time = when - SystemClock.uptimeMillis();
            long timeThreshold = BACKGROUND_MSG_THRESHOLD;
            if (foreground) {
                timeThreshold = FOREGROUND_MSG_THRESHOLD;
            }
            return time < timeThreshold;
        }
    } catch (Exception e){
        return false;
    }
    return false;
}
  1. 经过反射获取主线程LoopermMessage目标,该音讯的when变量,就表明该音讯的入队时刻
  2. 将入队时刻与当时时刻进行比较,就能够获取该音讯的等候时刻
  3. 当等候时刻超过必定阈值的话,咱们就认为主线程处于阻塞状况,结合 SIGQUIT 信号,判别为产生了 ANR

怎么定位 ANR 原因?

ANR 的影响要素有许多,咱们能够把他们分为以下几类:

  1. 体系资源缺乏,其它进程或线程存在严峻资源抢占,如 IO,Mem,CPU
  2. 线程间存在资源抢占,比方死锁等
  3. 主线程繁忙,用户输入得不到及时呼应

在将 ANR 原因分为了以上几类之后,咱们需求获取详细的日志信息,才能在产生 ANR 时及时定位原因

获取体系负载信息

想要获取体系负载信息,咱们在线下能够经过获取 /data/anr 目录下的 trace 文件来查看,可是在高版别手机上,咱们通常没有权限获取这个目录下的文件,那么在线上咱们该怎么获取体系负载信息呢?

使用层可经过 AcivityManager 获取 ProcessErrorStateInfo,如下所示:

  val am = application.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
  val processesInErrorStates = am.processesInErrorState

经过ProcessErrorStateInfo咱们能够获取shortMessagelongMessage,如下所示:

# shortMessage
ANR Input dispatching timed out (8445a92 com.android.test/com.android.test.anr.ANRActivity (server) is not responding. Waited 5000ms for MotionEvent(action=DOWN))

shortMessage便是产生 ANR 的原因,比较简单

# longMessage
ANR in com.android.test (com.android.test/.anr.ANRActivity)
PID: 23283
Reason: Input dispatching timed out (8445a92 com.android.test/com.android.test.anr.ANRActivity (server) is not responding. Waited 5000ms for MotionEvent(action=DOWN))
Parent: com.android.test/.anr.ANRActivity
ErrorId: 91ceb0ce-0af6-496e-8c4f-781075c056db
Frozen: false
Load: 0.0 / 0.29 / 0.33  # 表明 1, 5 和 15 分钟内的 CPU 平均负载
----- Output from /proc/pressure/memory -----
# avg10、avg60、avg300 别离代表 10s、60s、300s 的时刻周期内因内存资源阻塞的时刻百分比
# some 表明任一使命,full 表明一切非 idle 使命
some avg10=0.00 avg60=0.00 avg300=0.00 total=150136881
full avg10=0.00 avg60=0.00 avg300=0.00 total=51283028
----- End output from /proc/pressure/memory -----
CPU usage from 153ms to 605ms later (2023-05-04 22:38:19.034 to 2023-05-04 22:38:19.486):
  79% 1990/system_server: 35% user + 43% kernel / faults: 1598 minor
    43% 23375/AnrConsumer: 8.1% user + 35% kernel
    21% 2008/HeapTaskDaemon: 19% user + 2.7% kernel
    2.7% 2919/InputDispatcher: 2.7% user + 0% kernel
  32% 23283/com.android.test: 16% user + 16% kernel / faults: 7 minor
    28% 23315/RenderThread: 16% user + 12% kernel
    4% 23306/binder:23283_3: 0% user + 4% kernel
    4% 23354/binder:23283_5: 4% user + 0% kernel
  17% 1195/surfaceflinger: 17% user + 0% kernel
    10% 1195/surfaceflinger: 10% user + 0% kernel
    2.5% 1347/binder:1195_1: 2.5% user + 0% kernel
    2.5% 1414/TimerDispatch: 2.5% user + 0% kernel
  5% 1071/vendor.qti.hardware.display.composer-service: 2.5% user + 2.5% kernel
    2.5% 1071/composer-servic: 0% user + 2.5% kernel
  //...
21% TOTAL: 10% user + 10% kernel + 0.8% irq + 0.2% softirq

longMessage则是体系在产生 ANR 之后的一段时刻内的负载信息,包含 CPU,IO,内存等

一起也能够结合线上 logcat 的输出,例如假如ANR时刻点前后,日志里有打印 onTrimMemory,也能够作为内存严重的一个参阅判别,则此 ANR 或许由内存缺乏引起

获取进程内一切线程状况

产生 ANR 也或许是因为进程内的线程产生了资源抢占或者死锁,那么该怎么获取进程内一切线程的状况,咱们能够再看一下上面的这张图

【 Android 性能优化】ANR 问题如何监控?

Signal Catcher 的 Dump 产生在使用进程,并且经过 Socket Writer 来写 Trace的。假如咱们能够在这个 write 办法上进行 Hook,就能够获取到体系记载下来的 ANR Trace 内容。这个内容十分全面,包含了一切线程的各种状况、锁和仓库信息(包含 native 仓库),对于排查问题十分有协助,特别是一些与 native 问题、死锁等有关的问题。

Matrix 便是经过这种办法来获取 ANR Trace 的,详细完成可见:微信Android客户端的ANR监控计划

定位主线程问题

由主线程繁忙引起的 ANR 定位困难的原因在于:耗时或许由前史音讯引起,产生 ANR 时正在履行的音讯并不耗时。如下图所示:

【 Android 性能优化】ANR 问题如何监控?

图片来源:今天头条 ANR 优化实践系列 – 规划原理及影响要素

假如在体系服务履行某个前史音讯时,已经耗费了很多时刻,但在这个音讯履行完毕后,体系服务并没有达到触发 ANR 超时的临界点,之后的主线程持续调度其他音讯时,假如此刻体系判定呼应超时,那么正在履行的事务场景将不幸被射中。此刻,当时正在履行的事务逻辑并不复杂,但由于之前的某个音讯过度耗时,导致体系未能及时呼应后续事件。

针对这类问题,一个解决计划是:记载主线程过去一段时刻(比方 10s)内一切音讯的调度前史,保存事务方需求的要害数据(比方音讯耗时或者办法耗时),在产生 ANR 时,上报监控阈值内的前史数据

经过这种办法,在 ANR 产生时,能够回放过去一段时刻的耗时办法,定位耗时原因

头条和虾皮都根据这种思路开发了一些 ANR 监控东西,概况可见:今天头条 ANR 优化实践系列 – 监控东西与剖析思路与Android 卡顿与 ANR 的剖析实践

这些东西目前都没有开源,也有开发者根据头条的思路开源了一套完成,感兴趣的同学能够看看:app卡顿系列四 :今天头条卡顿监控计划落地

总结

本文主要是对 Matrix,今天头条等 ANR 监控计划的学习,介绍了体系 ANR 机制的作业流程,以及怎么监控 ANR 问题,ANR 问题产生时怎么定位详细原因等内容

ANR 问题定位困难的原因常常在于信息缺乏,经过上面介绍的获取体系负载信息,获取进程内一切线程状况,定位主线程问题等计划,应该能够比较有效地还原现场,协助定位 ANR 问题,期望对你有所协助~

参阅资料

微信Android客户端的ANR监控计划
今天头条 ANR 优化实践系列 – 监控东西与剖析思路