这是流畅性三板斧的第三篇文章,阅读前两篇会对理解该篇会有帮助,建议一起食用
ANR(Application Not responding):如果 Android 应用的界面线程处于阻塞状态的时间过长,会触发“应用无响应”(ANR) 错误。如果应用位于前台,系统会向用户显示一个对话框。ANR 对话框会为用户提供强制退出应用的选项。
ANR 是一个问题,因为负责更新界面的应用主线程无法处理用户输入事件或绘制操作,这会引起用户的不满
以上是官方对 ANR 的描述,ANR 的目的是从系统层面对严重影响用户直接感知的输入和界面绘制操作的行为进行的一种自我保护。
在系统层面触发 ANR 的就只有四个节点。也就是说即时主线程有一个很长的耗时任务,如果没有触发 ANR 产生的条件也不会产生 ANR。
出现以下任何情况时,系统都会针对您的应用触发 ANR:
以 BroadcastReceive 超时为例,看下系统如何触发 ANR
BroadcastReceiver 处理广播的核心逻辑位于BroadcastQueue
中
public final class BroadcastQueue {
final void processNextBroadcastLocked(boolean fromMsg, boolean skipOomAdj) {
setBroadcastTimeoutLocked(timeoutTime);
...
performReceiveLocked(...);//内部最终会调用BroadcastReceiver的onReceiver
...
cancelBroadcastTimeoutLocked();//解除超时
..
}
// 设置超时
final void setBroadcastTimeoutLocked(long timeoutTime) {
if (!mPendingBroadcastTimeoutMessage) {
Message msg = mHandler.obtainMessage(BROADCAST_TIMEOUT_MSG, this);
mHandler.sendMessageAtTime(msg, timeoutTime);
mPendingBroadcastTimeoutMessage = true;
}
}
//解除超时
final void cancelBroadcastTimeoutLocked() {
if (mPendingBroadcastTimeoutMessage) {
mHandler.removeMessages(BROADCAST_TIMEOUT_MSG, this);
mPendingBroadcastTimeoutMessage = false;
}
}
以上是广播结束者设置、解除、触发 ANR 的核心逻辑。通过 handler 机制延迟发送一个【ANR 任务】,在规定时间内完成了你广播接收者任务移除 ANR 任务。否则触发。
实际无论何种条件触发了 ANR 最终都交给 AnrHelper 处理,这个类中核心处理 ANR 的逻辑开启一个名为“AnrConsumer”的线程。执行在 ProcessErrorStateRecord 中appNotResponding()
的方法。
void appNotResponding(String activityShortComponentName, ApplicationInfo aInfo,
String parentShortComponentName, WindowProcessController parentProcess,
boolean aboveSystem, String annotation, boolean onlyDumpSelf) {
ArrayList<Integer> firstPids = new ArrayList<>(5);
SparseArray<Boolean> lastPids = new SparseArray<>(20);
...
setNotResponding(true);//标记ANR标识
...
firstPids.add(pid);
...
isSilentAnr = isSilentAnr();//后台的应用发生ANR
if (!isSilentAnr && !onlyDumpSelf) {//前台进程和不仅仅dump自身时
mService.mProcessList.forEachLruProcessesLOSP(false, r -> {
...
firstPids.add(pid);//添加其他进程
}
});
...
StringBuilder report = new StringBuilder();
report.append(MemoryPressureUtil.currentPsiState());//内存信息
ProcessCpuTracker processCpuTracker = new ProcessCpuTracker(true);//cup信息
nativePids.add(..); //添加native进程
...
File tracesFile = ..
report.append(tracesFileException.getBuffer());
info.append(processCpuTracker.printCurrentState(anrTime));
if (tracesFile == null) {
Process.sendSignal(pid, Process.SIGNAL_QUIT); //不dump信息时直接发送SIGNAL_QUIT信号
}
...
File tracesFile = ActivityManagerService.dumpStackTraces(..); //dump栈
...
mService.mUiHandler.sendMessageDelayed(msg, anrDialogDelayMs);//ANR弹窗
}
值得注意的点
Process.sendSignal(pid, Process.SIGNAL_QUIT);
系统会发出 Process.SIGNAL_QUIT 信号。这个很重要(图来自微信团队)
当应用发生 ANR 之后,系统会收集许多进程,来 dump 堆栈,从而生成 ANR Trace 文件,收集的第一个,也是一定会被收集到的进程,就是发生 ANR 的进程,接着系统开始向这些应用进程发送 SIGQUIT 信号,应用进程收到 SIGQUIT 后开始 dump 堆栈。
Android M(6.0) 版本之后,应用侧不能直接通过监听 data/anr/trace
文件,监控是否发生 ANR。
该方案我们在卡顿监控文章里也介绍过,主要思路就是超时检测,检测主线程 MessageQueue 在规定时间(5s)内是否处理了给定的消息。如果规定时间内没有处理掉给定的消息就认为发生了 ANR。
该方案用于检测 ANR 的弊端:
ToucEvent
未被消费发生 ANR 的条件。其他的产生 ANR 的条件的并不是 5s;系统发生 ANR 的时候,发出SIGQUIT
信号,通过监听这一信号,我们可以判断 ANR 的发生。该方案也是市面上监听 ANR 的主要方案。
除 Zygote 进程外,每个进程有SignalCatcher
线程,捕获 SIGQUIT 信号然后做相应的动作。Android 默认把 SIGQUIT 设置为 BLOCKED,这意味着只能只有SignalCatcher
线程能监听到SIGQUIT
信号,我们注册sigaction
监听不到。我们把SIGQUIT
设置为 UNBLOCK 这样就可能收到信号。但要注意,需要讲信号重新发送出去,不破坏系统的机制。
系统发出SIGQUIT
信号不一定该应用发生了 ANR,其他情况下也会发出’SIGQUIT’信号,比如其他进程发生了 ANR
源码里找答案
private void makeAppNotRespondingLSP(String activity, String shortMsg, String longMsg) {
setNotResponding(true);
// mAppErrors can be null if the AMS is constructed with injector only. This will only
// happen in tests.
if (mService.mAppErrors != null) {
mNotRespondingReport = mService.mAppErrors.generateProcessError(mApp,
ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING, //把发生ANR的进程
activity, shortMsg, longMsg, null);
}
startAppProblemLSP();
mApp.getWindowProcessController().stopFreezingActivities();
}
发生 ANR 时系统会把发生 ANR 的进程标记 NOT_RESPONDING
,我们可以在应用层通过 ActivityManager check 该状态,代码乳如下:
private static boolean checkErrorState() {
try {
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){
}
return false;
}
当收到SIGQUIT
信号之后,在一个时间段内不断的 check 该状态,如果获取到了该标识,就可以认为当前进程发生了 ANR。
有些 ANR 发生不会把进程不会设置NOT_RESPONDING
标识
应对方案:结合主线程的卡顿状态。
反射获取主线程MessageQueue
的 mMessages
对象,此对象的 when 变量时预期该消息被处理的时间,该变量和当前时间做差就能得到该消息等待的时间,被耽误的耗时,如果该耗过长,就说明主线程‘卡顿’了。
收到了SIGQUIT
且当前主线程卡顿了,就认为发生了 ANR。
通过监听系统SIGQUIT
信号结合 check 当前进程的 NOT_RESPONDING
标识和主线程的卡顿状态,综合判定为该进程发生了 ANR。
这只是我们知道发生了 ANR,知道发生了 ANR,进一步知道什么导致了 ANR,采集 ANR 发生时的上下文环境信息,解决 ANR 更重要。
观察 ANR 信息采集的难点在于往往信息采集不准确、不全面,当 ANR 发生的当下采集的信息并不是 ANR 的真正诱因,因而采集的信息对排查问题的参考价值大折扣。
如上图所示,在主线程耗时的任务已经执行完毕,service 启动任务在超过了规定的阈值产生了 ANR,此时采集的信息是一个正常的任务调用信息。
总的来说,诱发 ANR 的原因分主线程执行耗时过大和系统负载过重。
主线程任务执行耗时过大又可大概分为一下几种
系统负载过重,包括系统内存不足,cpu 负载,导致任务得不到执行。
如果能较完整记录过完一段时间段内主线程历史消息任务、当前执任务和将要执行的任务以及系统负载情况,对我们更为准确的诊断 ANR 问题有非常重要的意义。
主线程中记录监控 Looper 消息执行方案,我们自然的把目光转移到了 Looper 的 Printer 方案上。关于这个在三板斧的前两篇文章都介绍过这里不展开。
Looper 分发消息执行的时候,前后都打印消息信息,我们依此可以获取到消息任务的相关信息,包活 Message 的 target、
what、
callback、消息执行的耗时等。
消息耗时,需要采集主线程的 WallTime 和 ThreadTime。
大部分情况下,消息执行都耗时较短,Looper 也会有 Idel 状态,即无消息执行的状态,我们需要对这些消息进行聚合处理。
此外,三板斧系列文章主线程耗时监控里有介绍主线程处理消息除了 Looper 正常的分发的消息需要监控外,IdleHandler、TouchEvent 消息也要纳入到统计记录里才更为完整。
综合来说,把消息分类型,聚合类型(Agree),连续的耗时较少的消息。耗时类型(Huge):超过 50ms 的消息。系统调用消息(SYSTEM)
除了,统计记录 Looper Messge 的 what、callback 和耗时之外,每个消息内到底执行了什么哪些动作也需要采集,这就需要采集每个消息的执行的栈,频繁的采集执行栈对性能影响较大,要进行有策略的采集。
除了监控 ANR 发生之前主线程历史消息调度及耗时之外,也需要知道 ANR 发生时正在调度的消息及其耗时,以便于在看到 ANR 的 Trace 堆栈时,可以清晰的知道当前 Trace 逻辑到底执行了多长时间
MessageQueue 中等待执行的消息也很有必要统计
以上我们比较全面的监控统计主线程调度任务的耗时,
应用层可通过 AcivityManager 获取 ANRInfo
val am = application.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val processesInErrorStates = am.processesInErrorState
ProcessesInErrorState 中可获取到
shortMsg: ANR Inputdispatching timed out ...
Reason: Input dispatching timed out
xxx is not responding. Waited 5003ms for FocusEvent(hasFocus=false))
Load: 3.19 / 2.46 / 2.42
----- Output from /proc/pressure/memory -----
some avg10=0.00 avg60=0.00 avg300=0.00 total=144741615
full avg10=0.00 avg60=0.00 avg300=0.00 total=29370150
----- End output from /proc/pressure/memory -----
CPU usage from 0ms to xxx ms later with 99% awake:
TOTAL: 6.4% user + 8.2% kernel + 0% iowait + 0.6% irq + 0.1% softirq
CPU usage from 0ms to xxx ms later with 99% awake:
27% TOTAL: 10% user + 14% kernel + 0.3% iowait + 0.9% irq + 0.3% softirq
应用运行时采集 logcat 日志,注意需要控制量有相应的策略,定时清理。
String cmd = "logcat -f " + file.getAbsolutePath();
Runtime.getRuntime().exec(cmd);
从 Java 层获取各线程堆栈,或通过反射方式获取到虚拟机内部 Dump 线程堆栈的接口,在内存映射的函数地址,强制调用该接口,并将数据重定向输出到本地。
这样当 ANR 发生时我们就有丰富的信息供我们参考,包括主线程过去现在和未来的调度信息,系统的信息,线程信息,Logcat 信息。
查看我们主线程任务调度,是否有明显的耗时任务执行,诱发 ANR 产生。
Load: xx/xx/xx
ANR 发生的前 1 分钟 5 分钟和 15 分钟时间段内的 CPU 负载,数值代表着等待系统调度的任务数,数值过高,意味着系统有 CPU 和 IO 竞争激烈,我们的应用进程可能收到影响
CPU usage from 0ms to xxx ms later with xx% awake:
14% 1673/system_server: 8% user + 6.7% kernel / faults: 12746 minor
13% 30829/tv.danmaku.bili: 7.3% user + 6.2% kernel / faults: 24286 minor
6.6% 31147/tv.danmaku.bili:ijkservice: 3.7% user + 2.8% kernel / faults: 11880 minor
6% 574/logd: 2.1% user + 3.8% kernel / faults: 64 minor
..
TOTAL: 6.4% user + 8.2% kernel + 0% iowait + 0.6% irq + 0.1% softirq
CPU usage from xxms to xxxms later
73% 1673/system_server: 49% user + 24% kernel / faults: 1695 minor
33% 2330/AnrConsumer: 12% user + 21% kernel
15% 1683/HeapTaskDaemon: 15% user + 0% kernel
9.2% 7013/Binder:1673_12: 6.1% user + 3% kernel
6.1% 1685/ReferenceQueueD: 6.1% user + 0% kernel
6.1% 2715/HwBinder:1673_5: 6.1% user + 0% kernel
3% 2529/PhotonicModulat: 0% user + 3% kernel
25% 30829/tv.danmaku.bili: 4.2% user + 21% kernel / faults: 423 minor
25% 31050/thread_ad: 4.2% user + 21% kernel
...
...
27% TOTAL: 10% user + 14% kernel + 0.3% iowait + 0.9% irq + 0.3% softirq
如上,表示 ANR 发生前后的 CPU 占用情况,以及这些进程具体占用情况。
注意:单进程 CPU 的负载并不是以 100%为上限,而是有几个核,就有百分之几百,如 8 核上限为 800%
另外,kswapd
和 mmcqd
系统关键线程 CPU 线程过大,往往伴随系统回收资源,影响到应用进程
通过对 ANR 信息的解读可以更佳全面的帮助我们定位 ANR 问题。
线上如果记录了 Logcat 打印消息,着重从以下方面去看问题
onTrimeMemory
:连续的 onTrimMemory 往往说明 APP 的内存不足可能系统资源不足造成 ANRSlow operation
Slow delivery
出现该情况系统性能受限。从 ANR 产生的原因,系统处理 ANR,应用层监听 ANR,应对 ANR 应用侧比较全面的监控主线程的任务调度以及为了解决 ANR 采集系统信息最后给出了分析解决 ANR 问题的总体思路。
针对 ANR 问题,这些还远远不够,这里只是一个总体的框架,希望本文更全面的看待解决 ANR 问题有帮助。
到这里 Android 流程性三板斧就就结束了。
希望对你有帮助,不足错误之处海涵指正.