Skip to content

ANR:五种超时类型、检测链路、trace 阅读与真实案例 ​

本页导读:ANR 不是「App 没响应」这么直觉——它是五套独立计时器之一的到期通知。本页讲清每种超时的时长与触发者、system_server 侧的检测与现场抓取实现、/data/anr trace 的正确读法,用三个真实案例演练归因,最后给出监控工具链与复盘模板。前置:进程模型、Activity、架构篇 Handler 页。

一、五种 ANR:时长、计时者与文案 ​

类型经典超时计时者典型文案(logcat / dropbox)
输入事件5s(input dispatching timeout,以 AOSP 为准)InputDispatcherInput dispatching timed out ... Waiting because no window
广播历史前台 10s/后台 60s;现代队列为动态 anrTimeoutAnrHelper(现代广播队列)Broadcast of Intent { act=... }
服务执行前台 20s / 后台 200sActiveServicesexecuting service com.x/.Y
前台服务转正起 FGS 后未限期 startForeground(10s 量级)ActiveServicesContext.startForegroundService() did not then call startForeground()
ContentProvider 发布publish 超时(10s 量级,以 AOSP 为准)ContentProviderHelper/AMSContentProvider publish timed out

三个容易答错的点:

  1. 「Activity 生命周期超时」没有独立计时器——它借 input/broadcast 的壳发作(onResume 卡住 → 后续 input 分发不到 → input ANR);
  2. 超时是兜底上界不是精确闹钟:现代实现按进程状态动态调整(如 cached 接收者、低优先级场景放宽/收紧,细节以 AOSP 为准);
  3. 「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(...)

读栈顺序(背下来):

  1. 先看 main 线程:state 字段定性——Blocked(等锁,找 held by)、Native/Waited(等 IO/GC/binder)、Runnable(真在算,看栈顶函数);
  2. 锁等待闭环:waiting to lock ... held by tid=N → 翻到 tid=N 看它在干什么(网络/IO/另一把锁);
  3. Binder 线程池:全文搜 Binder: 线程——>15 个全忙且栈相似 → Binder 池耗尽(主线程同步 binder 调用也会被拖死);
  4. GC 与线程数:trace 头部有堆与线程摘要;>500 线程的应用常见「调度饥饿型 ANR」;
  5. 时间戳对照: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 排序) ​

  1. 主线程纪律:StrictMode 常开(检测磁盘/网络主调)+ Code Review 红线;
  2. 启动链路审计:Application/首屏 provider 的同步工作量表化治理;
  3. 广播 receiver:只做转发与轻量动作,重活交 WorkManager/goAsync + 超时自兜底;
  4. 锁:跨线程锁必须有「持锁方最坏耗时」评审;静态锁优先消灭;
  5. Binder 面:所有回调即转投;服务端 binder 线程池负载监控(线程名 Binder: 计数与栈采样);
  6. 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 变成一条可回归的工单。

参考链接 ​

基于 Android 16 / API 36 · 内容为原创整理,转载请注明出处