sequenceDiagram
SystemServer->>SystemServer: AppNotResponding.run
activate SystemServer
Note right of SystemServer: 1.非异常场景下,统计进程信息,包括:
Note right of SystemServer: 发生ANR的进程,SystemServer进程,SurfaceFligner进程,CPU占用高的5个进程
Note right of SystemServer: 2.打印ANR顶部信息
被统计的进程->>被统计的进程: 虚拟机启动过程在SignalCatcher进行信号注册和监听(SIGQUIT)
SystemServer->>被统计的进程: 3.逐个向统计的进程发送SIGQUIT信号,通知dump进程信息
被统计的进程->>被统计的进程: TheadList::DumpForSigQuit
activate 被统计的进程
Note right of 被统计的进程: 遍历所有的Java线程和虚拟机线程,设置checkpoint,请求挂起suspend
deactivate 被统计的进程
被统计的进程->>被统计的进程: 所有线程挂起后,SignalCatcher线程开始遍历Dump各线程的堆栈和线程数据,结束之后再唤醒线程
SystemServer->>SystemServer: 监听管道并读取管道内容,写入Trace文件
deactivate SystemServer
继续以广播接收为例,在上面介绍到当判定超时后,会调用系统服务 AMS 接口,搜集本次 ANR 相关信息并存档(data/anr/trace,data/system/dropbox),入口如下。
进入系统服务 AMS 之后,AppError 先进行场景判断,以过滤当前进程是不是已经发生并正在执行 Dump 流程,或者已经发生 Crash,或者已经被系统 Kill 之类的情况。并且还考虑了系统是否正在关机等场景,如果都不符合上述条件,则认为当前进程真的发生 ANR。
接下来系统再判断当前 ANR 进程对用户是否可感知,如后台低优先级进程(没有重要服务或者 Activity 界面)。
然后开始统计与该进程有关联的进程,或系统核心服务进程的信息;例如与应用进程经常交互的 SurfaceFligner,SystemServer 等系统进程,如果这些系统服务进程在响应时被阻塞,那么将导致应用进程 IPC 通信过程被卡死。
首先把自身进程(系统服务 SystemServer)加进来,逻辑如下:
接着获取其它系统核心进程,因为这些服务进程是 Init 进程直接创建的,并不在 SystemServer 或 Zygote 进程管理范围。
在搜集完第一步信息之后,接下来便开始统计各进程本地的更多信息,如虚拟机相关信息、Java 线程状态及堆栈。以便于知道此刻这些进程乃至系统都发生了什么情况
系统为何要收集其它进程信息呢?因为从性能角度来说,任何进程出现高 CPU 或高 IO 情况,都会抢占系统资源,进而影响其它进程调度不及时的现象。下面从代码角度看看系统 dump 流程:
private static void dumpStackTraces(String tracesFile, ArrayList<Integer> firstPids, ArrayList<Integer> nativePids, ArrayList<Integer> extraPids,
boolean useTombstonedForJavaTraces) {
......
......
//考虑到性能影响,一次dump最多持续20S,否则放弃后续进程直接结束
remainingTime = 20 * 1000;
try {
......
//按照优先级依次获取各个进程trace日志
int num = firstPids.size();
for (int i = 0; i < num; i++) {
final long timeTaken;
if (useTombstonedForJavaTraces) {
timeTaken = dumpJavaTracesTombstoned(firstPids.get(i), tracesFile, remainingTime);
} else {
timeTaken = observer.dumpWithTimeout(firstPids.get(i), remainingTime);
}
remainingTime -= timeTaken;
if (remainingTime <= 0) {
//已经超时,则不再进行后续进程的dump操作
return;
}
}
}
}
//按照优先级依次获取各个进程trace日志
for (int pid : nativePids) {
final long nativeDumpTimeoutMs = Math.min(NATIVE_DUMP_TIMEOUT_MS, remainingTime);
final long start = SystemClock.elapsedRealtime();
Debug.dumpNativeBacktraceToFileTimeout(
pid, tracesFile, (int) (nativeDumpTimeoutMs / 1000));
final long timeTaken = SystemClock.elapsedRealtime() - start;
remainingTime -= timeTaken;
if (remainingTime <= 0) {
//已经超时,则不再进行后续进程的dump操作
return;
}
}
}
//按照优先级依次获取各个进程trace日志
for (int pid : extraPids) {
final long timeTaken;
if (useTombstonedForJavaTraces) {
timeTaken = dumpJavaTracesTombstoned(pid, tracesFile, remainingTime);
} else {
timeTaken = observer.dumpWithTimeout(pid, remainingTime);
}
remainingTime -= timeTaken;
if (remainingTime <= 0) {
//已经超时,则不再进行后续进程的dump操作
return;
}
}
}
}
......
}
出于安全考虑,进程之间是相互隔离的,即使是系统进程也无法直接获取其它进程相关信息。因此需要借助 IPC 通信的方式,将指令发送到目标进程,目标进程接收到信号后,协助完成自身进程 Dump 信息并发送给系统进程。以 AndroidP 系统为例,大致流程图如下:
关于应用进程接收信号和响应能力,是在虚拟机内部实现的,在虚拟机启动过程中进行信号注册和监听(SIGQUIT),初始化逻辑如下:
SignalCatcher 线程接收到信号后,首先 Dump 当前虚拟机有关信息,如内存状态,对象,加载 Class,GC 等等,接下来设置各线程标记位(check_point),以请求线程起态(suspend)。其它线程运行过程进行上下文切换时,会检查该标记,如果发现有挂起请求,会主动将自己挂起。等到所有线程挂起后,SignalCatcher 线程开始遍历 Dump 各线程的堆栈和线程数据,结束之后再唤醒线程。期间如果某些线程一直无法挂起直到超时,那么本次 Dump 流程则失败,并主动抛出超时异常。
根据上面梳理的流程,SignalCatcher 获取各线程信息的工作过程,示意图如下:
到这里,基本介绍完了系统设计原理,并以广播发送为例说明系统是如何判定 ANR 的,以及发生 ANR 后,系统是如何获取系统信息和进程信息,以及其他进程是如何协助系统进程完成日志收集的。
整体来看链路比较长,而且涉及到与很多进程交互,同时为了进一步降低对应用乃至系统的影响,系统在很多环节都设置大量超时检测。而且从上面流程可以看到发生 ANR 时,系统进程除了发送信号给其它进程之外,自身也 Dump Trace,并获取系统整体及各进程 CPU 使用情况,且将其它进程 Dump 发送的数据写到文件中。因此这些开销将会导致系统进程在 ANR 过程承担很大的负载,这是为什么我们经常在 ANR Trace 中看到 SystemServer 进程 CPU 占比普遍较高的主要原因。
How to read Dalvik SIGQUIT output
https://codezjx.com/2017/08/06/anr-trace-analytics/
ActivityManagerService
http://zhijianz.me/2017/07/11/ANR%E5%88%86%E6%9E%90-trace%E6%96%87%E4%BB%B6%E5%88%86%E6%9E%90/