线上分析 ANR 时,经常会遇到一份看起来互相矛盾的日志:系统明确记录了 Input dispatching timed out,但 Trace 里的主线程却停在 nativePollOnce,像是在安静地等待下一条消息。
如果只看主线程当前栈,很容易得出"应用没有卡住"的结论;如果只看某个醒目的函数名,又容易把采样时碰巧执行的代码当成根因。我后来把这类现场固定拆成三个连续问题:系统当时在等什么,Trace 拍到的是哪个时刻,现有证据能不能覆盖整个超时窗口。
先从 Reason 确认系统等待的对象,再对齐 ANR 判定与 Trace 采集时间,最后沿主线程、锁、Binder、窗口和系统资源补齐因果链。
一、先别急着看栈:系统当时在等什么
ANR 不是"主线程卡了 5 秒"的同义词。Android 会为不同的系统交互建立不同的响应约束,输入事件、Service 生命周期、前台服务启动、广播和 JobService 的监控者与完成条件都不相同。这些故障可能进入 ANR 处理,也可能以版本特定异常终止应用;排查入口必须由 Reason 或异常类型决定。
| 触发场景 | 系统在等待什么 | 第一条证据 |
|---|---|---|
| 输入调度超时 | 目标窗口建立、接收并完成输入分发 | Input dispatching timed out 后面的具体原因 |
| Service 执行超时 | onCreate()、onStartCommand()、onBind() 等执行完成回执 |
executing service、ServiceRecord 与主线程调用链 |
| 前台服务未及时前台化(结果随版本变化) | startForegroundService() 后及时进入前台并展示通知 |
ForegroundServiceDidNotStartInTimeException、系统版本与 startForeground() 调用时间 |
| 广播超时 | onReceive() 返回,或 goAsync() 对应的 PendingResult 完成 |
Receiver 时间线与 finish() |
| JobService 超时 | onStartJob()/onStopJob() 返回及相应通知要求 |
Android 版本、目标 SDK 与 JobService 回调 |
输入事件 5 秒是官方文档明确写出的典型超时阈值。前台服务也必须在调用 startForegroundService() 后几秒内通过 startForeground() 进入前台,但失败结果存在版本差异:早期 AOSP 实现会走 ANR,当前官方排障文档则以 ForegroundServiceDidNotStartInTimeException 说明这类失败。Service、广播和 JobService 的期限也会随 Android 版本、前后台状态、目标 SDK 及厂商实现变化。因此,固定超时表只能帮助建立概念,不能替代当次 Reason、异常类型和对应版本实现。
同样是输入 ANR,does not have a focused window 与"窗口已有焦点但事件处理不完"就是两条不同路径。前者意味着 InputDispatcher 一直等不到合法输入目标,主线程即使在采样时空闲也不矛盾;后者才需要优先检查事件消费、长消息、锁、Binder 或绘制。
二、跟着一次 Service 启动,看超时怎样成立
理解 ANR 最有效的方法,不是先背超时时间,而是跟着一次受监控操作走完"登记、调度、执行、回执"四步。以 Service 创建为例,应用调用 startService() 后,真正的超时监控与生命周期调度跨越了应用进程、system_server、Binder 和应用主线程。
text
应用进程
ContextImpl.startServiceCommon()
-> ActivityManager.getService().startService(...)
system_server
ActivityManagerService.startService()
-> ActiveServices.startServiceLocked()
-> realStartServiceLocked()
-> bumpServiceExecutingLocked() // 登记正在执行并安排超时检查
-> scheduleCreateService() // Binder 调度到应用进程
应用主线程
ActivityThread.handleCreateService()
-> Service.onCreate()
-> IActivityManager.serviceDoneExecuting(...)
system_server
serviceDoneExecutingLocked()
-> 执行嵌套归零并移出 executingServices
-> 进程中无待完成 Service 时取消超时检查
未按时完成
serviceTimeout(...)
-> 进入 ANR 采集与处理链
第一步是登记执行。 bumpServiceExecutingLocked() 不只是给某个 onCreate() 单独挂一个定时器。系统会维护 Service 的执行嵌套以及进程内正在执行的 Service 集合。创建、启动、绑定和销毁都可能改变执行状态。
第二步是跨进程调度。 scheduleCreateService() 通过 Binder 把工作交给应用进程,随后由 ActivityThread 把生命周期任务放到主线程执行。此时,排在它前面的主线程消息、同步 Binder、类加载、锁竞争或日志阻塞,都可能推迟真正的回调时间。
第三步是完成回执。 应用执行完生命周期后调用 serviceDoneExecuting()。只有对应执行真正结束,嵌套计数和执行集合才会收敛;进程内没有其他待完成 Service 时,系统才取消这条超时监控。
java
void beginExecuting(ServiceRecord r) {
r.executeNesting++;
r.app.executingServices.add(r);
scheduleServiceTimeoutLocked(r.app);
}
void doneExecuting(ServiceRecord r) {
if (--r.executeNesting == 0) {
r.app.executingServices.remove(r);
}
if (r.app.executingServices.isEmpty()) {
cancelServiceTimeoutLocked(r.app);
}
}
这段简化逻辑不对应某一 Android 版本的完整源码,但保留了定位时最重要的状态关系。Service ANR 不能被简化成"某个生命周期函数自身执行过久"。需要同时确认监控何时开始、Binder 调度是否到达、主线程何时真正执行、完成回执何时返回,以及同一进程中是否还有其他未完成的 Service。
三、系统已经判定超时,Trace 为什么还能拍到空闲主线程
现在再回到开头的矛盾现场。ANR 条件成立与线程 Trace 完成采集并不是同一个时刻,中间还要经过调度、信号、线程挂起、堆栈生成以及平台整理。
text
T0:真正的阻塞、无焦点窗口或系统异常开始
T1:达到系统响应期限,ANR 条件成立
T2:system_server 安排并完成线程 Trace 采集
T3:系统或第三方平台整理、压缩、上报
真正需要解释的是 T0 - T1
单份线程栈主要描述的是 T2
如果阻塞在 T1 之后解除,T2 就可能只拍到主线程重新进入 nativePollOnce。如果监控框架包裹了每次消息分发,采样也可能刚好停在它的公共入口。反过来,即使 T2 的栈顶出现业务函数,也必须确认它不是超时成立后刚开始执行的新消息。
因此,判断 Trace 是否拍中现场,至少要对齐 ANR Reason 的时间、CPU 统计区间、Trace 时间戳和最终上报时间。再把 Activity 生命周期、窗口焦点、消息 dispatch、Binder、GC 和业务日志放到同一时间轴上。时间对不上时,结论必须降级,而不是继续从单点栈里寻找确定答案。
四、拿到一份 ANR 日志,按这个顺序往下查
第一步:固定触发事实和时间。 保存完整 Reason、进程、组件、Android 版本、目标 SDK、系统 uptime、ANR 判定时间和 Trace 时间。第三方平台生成的 ANR_EXCEPTION 只能作为入口,不能替代系统 Reason。
第二步:读主线程头,但不要只读 Java 栈。 线程头同时包含 ART 状态、Linux 调度状态、系统线程 ID、累计运行与排队时间等信息。它们的时间口径不同,必须先读懂再使用。
text
"main" prio=5 tid=1 Runnable
| group="main" sCount=0 dsCount=0 flags=0
| sysTid=3309 nice=-10 cgrp=default sched=0/0
| state=R schedstat=( 48552284396 1834170006 63513 )
| utm=3596 stm=1258 core=4 HZ=100
| held mutexes= "mutator lock"(shared held)

| 字段 | 它能说明什么 | 不能怎样使用 |
|---|---|---|
Runnable / Native / Waiting |
ART 视角的线程状态 | 不能仅凭状态名确认正在耗 CPU 或发生死锁 |
state=R/S/D |
Linux 调度状态:运行或可运行、睡眠、不可中断等待 | 不能与 Java Thread.State 混为一谈 |
sysTid |
Linux 线程 ID;主线程通常与进程 PID 相同 | 不能把 ART 的 tid 当作系统 TID |
schedstat=(a b c) |
累计运行时间、累计 runqueue 等待时间、时间片次数 | 不能把进程启动以来的累计值当成本次 ANR 窗口 |
utm/stm |
累计用户态与内核态 CPU tick | 不能不看 HZ 就直接当成毫秒 |
core |
最近运行所在的 CPU 核 | 不能推断线程始终固定在该核 |
mutator lock(shared held) |
ART 访问托管堆时常见的运行时锁状态 | 不能直接解释为业务互斥锁未释放 |
示例中的 schedstat 大约表示主线程累计运行 48.55 秒、累计在运行队列等待 1.83 秒、经历 63513 个时间片。这有助于理解线程生命周期内的调度特征,却不能证明 1.83 秒全部发生在本次 ANR。判断超时窗口内是否长期得不到调度,需要 Perfetto 或相邻多次采样。
第三步:沿依赖继续追。 主线程如果在等待锁,要从 waiting to lock 或 waiting on 找到对象,再搜索哪个线程持有它,并继续追持有者是否在做 I/O、Binder、计算或等待另一把锁。只有等待图形成环,才能称为死锁;单向等待只是锁竞争。
主线程如果停在 BinderProxy.transactNative,只能确认同步调用尚未返回。下一步要识别具体 AIDL 接口、对端进程、对端 Binder 线程池和同期 system_server 栈。系统服务变慢与应用在主线程发起高风险同步调用可以同时成立,不能只选一边归责。
Reason 如果指向无焦点窗口,依赖对象就不是某把 Java 锁,而是 Activity、WindowManager、InputDispatcher 与窗口焦点状态。此时应检查 resumed/visible、window attach、首帧、转场、锁屏和系统窗口覆盖。
第四步:用 CPU、I/O 和内存数据验证候选。 这些数据的价值是缩小范围和提供反证,不是看到一个高值就直接宣布根因。
text
Load: 27.91 / 26.13 / 25.27
CPU usage from 26860ms to 0ms ago:
63% system_server: 46% user + 16% kernel / faults: ...
46% surfaceflinger: 27% user + 18% kernel / faults: ...
5.3% com.example.app: 4% user + 1.3% kernel / faults: ...
41% TOTAL: 23% user + 17% kernel + 0% iowait
Load 表示可运行和不可中断等待任务的平均数量,不是 CPU 百分比。在 8 核设备上,1 分钟 Load 接近 28 说明队列压力明显高于核心数,但它没有告诉我们任务是在争 CPU、等待内核资源,还是统计区间里出现了短时峰值。
41% TOTAL 表明采样区间仍有整体空闲容量;iowait=0% 也不支持"CPU 大部分时间都在等块设备"这一强判断。某进程的 major fault 较多同样只是线索,必须确认它是否集中在 ANR 时间窗,并与 I/O、swap 和调用栈相互印证。
text
Free memory until OOME 494MB
Total mutator paused time: 62.994ms
Total time waiting for GC to complete: 85.246ms
Total GC count: 152
Total blocking GC count: 2
Total blocking GC time: 172.679ms
Free memory until OOME 只描述应用堆距离上限的理论余量,不能代表系统整体内存。GC 总次数、累计 pause 和累计 blocking time 通常覆盖进程启动以来的生命周期,也不能直接搬到本次超时窗口。判断内存是否参与因果链,需要同时看 GC 时间线、系统可用内存、swap、major faults、LMKD 和同一时刻其他进程的异常。
第五步:把结论写到证据允许的位置。 Reason、时间窗与调用或依赖链闭环时,才能写"已确认根因";多条同期证据一致但缺一段关键链路时,只能写"高概率";单次栈、累计指标或关键词只能列为候选;Reason 与时间线缺失时,应明确写"现有证据不足"。
五、五种常见主线程现场,下一步应该看哪里
| 采样现场 | 当前能确认什么 | 下一步证据 |
|---|---|---|
| 业务计算、I/O 或日志输出 | 采样时主线程正在这条路径 | 消息耗时、连续采样、I/O 轨迹,确认它是否覆盖超时窗口 |
| 等待 Java 锁或结果 | 主线程依赖某个对象或条件 | 锁持有者、后续依赖与是否形成循环等待 |
BinderProxy.transactNative |
同步 Binder 尚未返回 | AIDL 方法、对端进程、Binder 线程池和调用时机 |
nativePollOnce |
采样时 Looper 在等待消息或 native 事件 | Reason 与 T0-T2 时间线,判断是恢复后的空闲还是窗口类超时 |
ConditionVariable::WaitHoldingLocks |
ART 内部条件变量等待 | 具体锁对象、持有者和闭环;不能仅凭函数名判定业务死锁 |
例如主线程出现下面这组日志帧,只能说明采样时正在同步格式化并写日志。大量堆栈拼接、logd 背压或对象锁竞争都可能放大耗时,但仍需消息耗时或连续采样证明它覆盖了 ANR 窗口。
text
at com.android.internal.os.LoggingPrintStream.flush(...)
at com.android.internal.os.LoggingPrintStream.println(...)
- locked (a com.android.internal.os.AndroidPrintStream)
at java.lang.Throwable.printStackTrace(...)
at android.os.Handler.dispatchMessage(...)
六、工具不在多,而在于补齐哪一段证据
bugreport 与系统 ANR Trace。 新系统通常在 /data/anr/anr_* 保存多份记录,部分旧版本可能使用 traces.txt。bugreport 还能补充 ActivityManager、WindowManager、InputDispatcher、CPU、内存和系统服务上下文。路径访问受版本、构建类型和权限限制,线上不能假设应用可直接读取。
shell
adb bugreport anr-bugreport.zip
adb shell ls /data/anr
adb pull /data/anr ./anr
ApplicationExitInfo。 Android 11(API 30)开始可以查询本应用的历史退出原因,部分 ANR 记录可通过 getTraceInputStream() 读取系统 Trace。Trace 位于有限的历史记录中,可能为空或被后续记录覆盖,应尽早读取、限长并关联准确时间。
kotlin
val am = getSystemService(ActivityManager::class.java)
val exits = am.getHistoricalProcessExitReasons(packageName, 0, 20)
exits.filter { it.reason == ApplicationExitInfo.REASON_ANR }
.forEach { info ->
info.traceInputStream?.bufferedReader()?.use { reader ->
val trace = reader.readText()
// 清理敏感字段、限长并关联发生时间后再持久化
}
}
Perfetto/System Trace。 线程 Trace 是单点快照,Perfetto 记录的是一段时间。线程调度、CPU 频率、Binder、I/O、窗口和应用自定义 trace section 可以回答"主线程为什么没有运行""Binder 对端在做什么""慢消息到底覆盖了多久"。Android 10 及以上优先使用 Perfetto。
开发期与线上埋点。 StrictMode 适合提前发现主线程磁盘或网络 I/O;线上慢消息记录至少应包含 dispatch 开始与结束、Message 目标、Activity 生命周期、窗口焦点和关联业务。监控本身必须轻量,不能在主线程同步打印完整堆栈或执行重型序列化,否则工具会成为新的干扰变量。
七、最终判断,只回到三件事
| 日志现象 | 容易越界的结论 | 必须补齐的证据 |
|---|---|---|
主线程在 nativePollOnce |
应用无责 | Reason 与 T0-T2 时间线 |
| 两个线程等待同一对象 | 已经发生死锁 | 锁持有者与循环等待关系 |
| 栈顶是监控框架 | 监控框架导致 ANR | 框架自身耗时覆盖超时窗口 |
| Load 很高 | CPU 已经用满 | 核心数、TOTAL、调度与 I/O 时间线 |
| 应用堆余量很大 | 不存在内存压力 | 系统内存、swap、LMKD 与 GC 时间线 |
一份可靠的 ANR 分析,最后只需要回答三件事:系统等待的对象是什么,真正的异常覆盖了哪段时间,哪条依赖链解释了超时。Reason 决定方向,时间线决定 Trace 是否有效,锁、Binder、窗口和系统资源决定因果链能否闭环。
暂时无法定位并不可怕。可怕的是用一个栈顶函数、一个累计指标或一个醒目的关键词制造出过早答案。只要明确现有证据能证明什么、不能证明什么,以及下一次应该补采什么,分析就仍然在向正确答案推进。
继续看案例: 如果你想看这套方法怎样落到真实 Trace 上,可以继续读:主线程明明是空闲的,为什么仍然 ANR?3 个真实 Trace 的证据链复盘。