Appearance
ANR:五种超时类型、检测链路、trace 阅读与真实案例
本页导读:ANR 不是「App 没响应」这么直觉——它是五套独立计时器之一的到期通知。本页讲清每种超时的时长与触发者、system_server 侧的检测与现场抓取实现、
/data/anrtrace 的正确读法,用三个真实案例演练归因,最后给出监控工具链与复盘模板。前置:进程模型、Activity、架构篇 Handler 页。
一、五种 ANR:时长、计时者与文案
| 类型 | 经典超时 | 计时者 | 典型文案(logcat / dropbox) |
|---|---|---|---|
| 输入事件 | 5s(input dispatching timeout,以 AOSP 为准) | InputDispatcher | Input dispatching timed out ... Waiting because no window |
| 广播 | 历史前台 10s/后台 60s;现代队列为动态 anrTimeout | AnrHelper(现代广播队列) | Broadcast of Intent { act=... } |
| 服务执行 | 前台 20s / 后台 200s | ActiveServices | executing service com.x/.Y |
| 前台服务转正 | 起 FGS 后未限期 startForeground(10s 量级) | ActiveServices | Context.startForegroundService() did not then call startForeground() |
| ContentProvider 发布 | publish 超时(10s 量级,以 AOSP 为准) | ContentProviderHelper/AMS | ContentProvider publish timed out |
三个容易答错的点:
- 「Activity 生命周期超时」没有独立计时器——它借 input/broadcast 的壳发作(onResume 卡住 → 后续 input 分发不到 → input ANR);
- 超时是兜底上界不是精确闹钟:现代实现按进程状态动态调整(如 cached 接收者、低优先级场景放宽/收紧,细节以 AOSP 为准);
- 「ANR = 崩溃」是错觉:弹窗出现时进程常还活着,trace 抓到的是「它正卡在哪」的现场快照。
二、检测实现:消息链与现场抓取
java
// frameworks/base/services/core/java/com/android/server/am/AnrManager.java(Android 12+ 的 ANR 中枢,机制大意)
// 各来源把「超时嫌疑」报进来:
// InputDispatcher → system_server 原生侧发现窗口队列滞留 → notifyAppNotResponding
// ActiveServices / BroadcastQueueModern(AnrHelper) 定时器到期 → appNotResponding
// AnrManager 统一裁决:
// 1) 检查进程是否已死亡/正在 debug(是则取消)
// 2) 组装 ANR 报告:原因、调度统计、CPU、am_anr 事件
// 3) 向目标进程发信号抓 ART 线程栈 → 写 /data/anr/
// 4) 决定弹不弹「无响应」对话框、要不要顺带 dump 兄弟进程关键工程事实:
- ANR 的判定主体全在系统侧:应用无法注册「我的 ANR 监听」;自研 watchdog 只能模拟(Looper printer 采样主线程消息耗时),判定权不在你。
- trace 抓取走 ART 的 debug 通道:向目标进程发
SIGQUIT类信号,VM 在每个线程安全点落栈——所以「栈全在 native 卡死」时 Java 栈可能只有nativePoll一类浅帧,要结合/proc/<pid>/task/*/stack(需 root)或 simpleperf 看原生栈。 - 输入 ANR 的完整链:
InputDispatcher 发现未消费输入超阈值 → 通知 AMS(appNotResponding) → AnrManager 抓栈/上报 → 对话框;期间 InputDispatcher 已把该窗口标记为「等待 ANR 答复」,这就是用户感觉「整屏没反应」。
ANR 报告解剖(一份 dropbox system_app_anr 条目的骨架):
text
Subject: executing service com.example/.HeavyService ← reason 行,决定归类
----- dumping pid 12345 at ... ----- ← /data/anr trace 引用
CPU usage from 92134ms to 5233ms ago (period: ~96% based on trace time)
15% 12345/com.example: 9% u + 6% k ← 应用 CPU 增量
not completing for process ... ← AMS 调度统计片段- CPU 增量是第一分叉口:user% 高 → 真在烧 CPU(算法重、死循环、GC 风暴);user% 低 → 线程没被调度(锁/IO/binder 等待),再翻栈看等什么;
- event log 有
am_anr事件(adb shell logcat -b events | grep am_anr),字段含 pid/包名/reason 摘要——线上采集管道通常从它起跳; - 抓栈是「全家福」:ANR 处理里系统可对同 UID 的兄弟进程一并 dump(trace 文件里会看到多段
----- pid ... -----),排查多进程应用时先看目标 pid 段,再看有没有被拖下水的兄弟段。
三、/data/anr trace 怎么读
text
----- pid 12345 at 2026-10-13 09:12:33.456 -----
Cmd line: com.example.app
----- main -----
"main" prio=5 tid=1 Blocked
at com.example.db.HugeLoader.readAll(HugeLoader.java:88)
- waiting to lock <0x0abc> (a com.example.db.Lock) held by tid=42
at android.app.ActivityThread.handleLaunchActivity(...) ← 栈底在生命周期 = 启动期卡死
----- tid=42 -----
"OkHttp Dispatcher" prio=5 tid=42 Native
at java.net.SocketInputStream.read(...) ← 持锁者在联网
at com.example.db.HugeLoader.fetch(...)读栈顺序(背下来):
- 先看 main 线程:
state字段定性——Blocked(等锁,找 held by)、Native/Waited(等 IO/GC/binder)、Runnable(真在算,看栈顶函数); - 锁等待闭环:
waiting to lock ... held by tid=N→ 翻到 tid=N 看它在干什么(网络/IO/另一把锁); - Binder 线程池:全文搜
Binder:线程——>15 个全忙且栈相似 → Binder 池耗尽(主线程同步 binder 调用也会被拖死); - GC 与线程数:trace 头部有堆与线程摘要;>500 线程的应用常见「调度饥饿型 ANR」;
- 时间戳对照:trace 抓于超时确认时,真凶往往是 ANR 前几秒的主线程消息——需要 perfetto 时间线补全(见第五节)。
补充:新版 trace 会附带 schedstat/CPU 时钟片段,能把「Blocked 了多久」直接读出来,比对照两个 trace 文件可靠得多(片段形态随版本有差异,以设备输出为准)。
常见栈形态速查表(对照主线程直接定性):
| main 栈形态 | 高频原因 | 下一步 |
|---|---|---|
| Blocked + waiting to lock | 锁被别的线程持有 | 顺 held by tid 看持锁者在干嘛 |
| Native + nativePollOnce | 主线程空闲但事件没到它手上 | 查 input 窗口/焦点/channel 是否被关闭 |
| Native + binder transact | 同步 binder 调系统等别人 | 看 system_server/目标服务是否拥堵 |
| RUNNABLE 栈顶是业务代码 | 纯计算超预算 | profiler 采样锁热点方法 |
| WAITING + LockSupport 一类 | join/await 等并发原语不返回 | 审并发结构与超时设置 |
| 全浅栈停在 libc/native | 卡在系统调用(IO/锁/页错误) | simpleperf 或 /proc/<pid>/task/*/stack |
四、三个真实案例
案例 A:启动期 IO 链(input ANR) 低端机冷启动首屏 12s 后弹 ANR。trace:main 栈顶 SQLiteOpenHelper.getWritableDatabase(升版迁移)← Application.onCreate 里同步预热带数据库缓存。归因:input 事件 5s 无人认领 + 生命周期借壳。修复:迁移后台化 + 首屏只读内存投影。教训:onCreate 的预算是「毫秒级」,不是「秒级」。
案例 B:广播里的锁反转(broadcast ANR) 推送 SDK 与业务共用一把静态锁:推送在 worker 持锁写缓存同时网络重试(30s+);业务高频广播 receiver 在 onReceive 里等同一把锁 → 连续 receiver 超时。trace:多个广播 tid Blocked, held by worker。修复:锁粒度拆到 key + receiver 改无锁快照读。教训:广播超时按 receiver 计,一把锁能把整条队列拖穿。
案例 C:Binder 池耗尽(service ANR 形态) 地图聚合页把瓦片解析放进 Binder 回调里同步等 IO,16 条 Binder 线程全挂;随后 startService 排在该进程 binder 事务队列之后 → executing service ANR。trace 特征:Binder:1234_1..16 全 Native 同栈。修复:回调即转投线程池(AIDL 面加 oneway/流控)。教训:Binder 池是全进程共享资源,滥用 binder 侧长任务 = 自断 IPC 动脉。
三个案例的共同规律:系统侧计时器只负责「到点报警」,病因永远是应用侧占用了主线程赖以推进的资源(CPU、锁、Binder 线程、调度)。归因时先问「主线程在等谁」,再问「谁让它等的」,两问都答上才算闭环。
五、监控与工具链
- perfetto(首选):配置启用
android.anr、sched、am、gfx等数据源(集以所用 perfetto 版本为准);ANR 时刻前后的主线程 slice、CPU 频率、调度等待一目了然——「为什么 5 秒没跑完」的时间线只有它能给全。 - bugreport:
adb shell bugreport汇总 /data/anr、dropbox(system_app_anr/anr条目)、广播/服务队列快照;线上用户回传的第一现场。 - Play vitals / 厂商基线:Android Vitals 的 ANR 率口径 = 异常会话占比,优化目标盯这个而非单次 trace。
- 自研探针:Looper printer 主线程消息耗时采样 + 帧回调(JankStats/FrameMetrics),只能做「疑似 ANR」预警,别宣称等价系统判定;
- 三方 APM(Bugly/Matrix 一类):套路 = 主线程桩采样 + ANR 高危点预判 + 自采栈上报,胜在能进线上灰度;注意其「大帧/卡死」口径与 Google Vitals 口径不同,两套数据别互相打架。
- 命令行速查:
bash
adb shell dumpsys dropbox --print | grep -i anr # ANR 事件清单与 reason 行
adb pull /data/anr ./anr_traces # trace 文件(需 root/eng 版)
adb shell "kill -3 $(adb shell pidof com.example)" # 需 root:抓栈快照到 /data/anr(≠ANR 判定)
adb shell logcat -b events | grep am_anr # 事件日志里的 ANR 记录六、预防清单(按 ROI 排序)
- 主线程纪律:
StrictMode常开(检测磁盘/网络主调)+ Code Review 红线; - 启动链路审计:Application/首屏 provider 的同步工作量表化治理;
- 广播 receiver:只做转发与轻量动作,重活交 WorkManager/
goAsync+ 超时自兜底; - 锁:跨线程锁必须有「持锁方最坏耗时」评审;静态锁优先消灭;
- Binder 面:所有回调即转投;服务端 binder 线程池负载监控(线程名
Binder:计数与栈采样); - CI 化:启动基准 + 帧率基准(Macrobenchmark 一类),把回归拦在合入前。
七、复盘模板:把一次 ANR 变成一条可回归的工单
text
1. 归类:五型中的哪一型?(reason 行 + 计时者,对照第一节表)
2. 定位:main 栈顶帧 + 锁持有者/Binder 池状态 → 归因到文件与行号
3. 定量:CPU 增量(忙/等)、发生次数与影响面(Vitals 会话占比)
4. 复现:固定路径(冷启动/低端机/弱网/压测脚本),复现不了的挂概率标签
5. 修复:改动必须挂回归因帧,禁止「顺手重构」
6. 回归:复现脚本进 CI,上线后盯 Vitals 曲线收敛- 归因要求一句话可检查:谁在什么线程做了什么阻塞、预算多少、实际多少;
- 修完必须能回答「为什么这次不会再超时」,答不上就是没修完;
- 同一 reason 连续两个版本不降 → 升级为架构问题(通常是主线程承担了对账/初始化职责)。
填写示例(接案例 B):
text
归因:推送SDK静态锁,worker 持锁做网络重试(最坏35s),广播 tid 等锁 >动态anrTimeout
预算:onReceive 同步段 <100ms,实际 >10s
修复:锁按 key 拆分 + receiver 无锁快照读
回归:压测脚本(每秒 5 条广播 + 弱网推送重试)进 CI;Vitals broadcast 类 ANR -82%八、常见误区问答
Q1:ANR 弹窗出现时进程会被杀吗? 用户点「等待」则进程继续;点「关闭应用」才走 finish。现代版本默认不打扰用户(屏灭、进程将死等场景只记录不打 dialog,策略以版本为准)。
Q2:主线程一直 sleep 会一直 ANR 吗? 不是无限弹:同类超时每个「事件/服务/广播记录」只计一次;ANR 窗口期内进程状态被重置后才会再触发。
Q3:watchdog 线程能救回 ANR 吗? 救不回判定(判定在系统侧),但能在「将死未死」时主动 finish/降级/上报,把弹窗变成无感恢复——这是商业 App 的标准兜底姿势。
Q4:native crash 会表现为 ANR 吗? 会。锁死主线程时 input 无人应答就是 ANR 而非 crash——trace 里 native 帧浅时务必结合 tombstone 与 simpleperf 交叉验证。
Q5:为什么用户没看到弹窗,后台却有 ANR 上报? 现代系统策略越来越不打扰:不满足弹 dialog 条件(屏灭、进程濒死、后台场景)时只写 /data/anr + dropbox,Vitals 照样计数。「ANR 率涨了但用户无投诉」完全正常。
Q6:多进程应用 ANR 算哪个进程? 超时对象在哪个进程就算哪个;但报表归因跟报告的 package/process 字段走。如果主 App 面板出现后台进程卡顿,先核对报告 process 字段是不是 :push 一类子进程。
版本注记:基于 Android 16 / API 36;Android 12 差异:ANR 裁决集中到
AnrManager,现代广播队列(BroadcastQueueModern)上线;Android 13+ 差异:trace 文件形态增强(schedstat/时钟片段)、后台 ANR 对用户打扰度持续下调;早期 8.x/9 时代的「广播 10s/60s」固定常量在设备上仍可能从旧文案看到,新机型一律按动态超时理解。
本页小结:ANR 是五套系统侧计时器的到期通知,时长和文案各有归属;检测与抓栈全在 system_server(AnrManager + debug 信号),应用侧只能模拟监测;读 trace 的顺序是 CPU 增量定性 → main 线程定性 → 锁闭环 → Binder 池 → perfetto 时间线补全;预防的本质是主线程预算制与「广播/回调不扛重活」的纪律,复盘的本质是把每次 ANR 变成一条可回归的工单。
参考链接
- ANR 官方指南:https://developer.android.com/topic/performance/anrs
- Android Vitals 口径:https://developer.android.com/topic/performance/vitals
- AnrManager(Android 12+ 的 ANR 中枢):https://cs.android.com/android/platform/frameworks/base/+/main:services/core/java/com/android/server/am/AnrManager.java
- InputDispatcher(输入 ANR 计时现场):https://cs.android.com/android/platform/frameworks/native/+/main:services/inputflinger/InputDispatcher.cpp
- 站内:进程模型、Activity、Service、广播、架构篇 Binder、Handler 与 Looper