ANR监控与治理
# ANR 监控与治理
📊 学习成本预估 | 难度:⭐⭐⭐⭐⭐(5/5)| 阅读:约 50 分钟 | 实操:3 小时 🔗 前置阅读:卷三·03 | ➡️ 后续延伸:卷五·01
# 目录介绍
- 01.阅读说明
- 02.贯穿案例
- 03.ANR 本质定义
- 04.触发条件原理
- 05.度量与采集
- 06.归因决策树
- 07.输入事件全链路 ⭐
- 08.Service 全链路 ⭐
- 09.Broadcast 全链路 ⭐
- 10.ContentProvider 全链路 ⭐
- 11.iOS Watchdog 全链路 ⭐
- 12.跨端 ANR 对照
- 13.治理一层减负 ⭐
- 14.治理二层协同 ⭐
- 15.治理三层监控 ⭐
- 16.治理四层组件 ⭐
- 17.求证实验 ⭐
- 18.实战案例
- 19.防劣化体系
- 20.跨平台速查
- 21.总结与延伸
# 01.阅读说明
- 本文卷归属:卷三 · 流水线专项 · 第 4 篇
- 本文目标层级:L2 进阶 → L3 专家
- 适用平台:Android(主) / iOS(watchdog) / Web(页面无响应) / 嵌入式(看门狗复位)
- 前置阅读:
卷三·03 卡顿捕获与归因(ANR 是卡顿的极端形态)卷二·04 线程模型与调度优化(线程问题导致 ANR)
- 本文核心命题:
ANR = 卡顿的极端形态,是"主线程被阻塞超过系统忍耐阈值"的兜底惩罚。 一切 ANR 治理 = 卡顿治理 + 兜底机制 + 现场归因。 ANR 不是新问题,是已被忽视的卡顿问题的最终爆发——这是 §02 案例 经验派 3 周失败的核心教训。
# 02.贯穿案例
本案例贯穿全文:§03 看懂物理本质、§04 用触发模型量化、§07-§11 拆解各类 ANR 原理、§17 用实验复盘、§13-§16 给出分层闭环。
# 2.1 案例背景
某头部电商 App V12.3 大促前压测一切正常,正式上线后凌晨 0:00 抢购开始的 5 分钟内:
- Google Play Console 的 ANR 率从 0.08% 飙升到 0.52%,被打"过度的应用未响应"标签,应用商店推荐位被降权。
- iOS Watchdog 启动率从 0.03% 升到 0.21%。
- 微博话题 #XX商城卡死# 上热搜,1 小时内 8000+ 投诉。
- 经济损失粗算:可推算的订单流失约单日 2300 万。
研发组调出系统 /data/anr/traces.txt:栈顶 70% 是 nativePollOnce,30% 在 Object.wait。"看起来主线程都在等消息,怎么会 ANR?"
# 2.2 经验派 3 周折腾(典型反面教材)
| 周次 | 动作 | 结果 |
|---|---|---|
| 第 1 周 | 把首页接口超时从 30s 改到 5s(怀疑网络) | 变化不大,新增大量"加载失败"投诉 |
| 第 2 周 | 加了 try-catch 包住 onCreate(怀疑异常) | ANR 率不降反升(异常被吞,问题更难定位) |
| 第 3 周 | 把 ANR 监控从 5s 改 10s(自欺欺人) | 上报数下降,但用户骂声更大(系统仍按 5s 杀进程) |
复盘:三周里所有动作都基于"主线程在等消息(nativePollOnce)= 主线程没在干活 = 不是我们代码的问题"的错误推理。这恰恰是 §04 反直觉问题 ③ 的反面教材——主线程没卡 CPU 但仍 ANR,根本原因是 off-CPU 等待。
# 2.3 方法派 6 天闭环
新接手的同学按本文方法论重做:
Day 1(§04 第一性原理):用"触发条件模型"重新看 trace。系统 traces.txt 只是"5s 兜底时刻的快照",看不到5s 之内主线程经历了什么。需要 §3 方案①+② 还原过程。
Day 2(§05 三方案组合):上线 LooperPrinter(方案①)+ WatchDog(方案②,阈值 2s)。上线 6 小时抓到 1240 次准 ANR,所有线程栈都被记录。
Day 3(§06 归因决策树):分析 1240 个准 ANR 现场,发现明确模式:
| 模式 | 占比 | 主线程栈顶 | 真正持锁/阻塞线程 |
|---|---|---|---|
| 模式 A:DB 锁竞争 | 42% | nativePollOnce | DB 写入线程持 SQLite 写锁 + 网络下载 |
| 模式 B:Binder 远程慢 | 28% | Binder.transact | system_server 在大促时变慢 |
| 模式 C:HTTP 客户端单连接池等 | 18% | Object.wait | OkHttp 连接池排队 |
| 模式 D:onCreate 串行 SDK | 12% | 业务方法 | 推送/统计/支付 SDK 同步 init |
→ 70% 是 off-CPU 类 ANR——主线程没在烧 CPU,但在等其他线程/进程。这是 §04 触发模型 "off-CPU 主导"的真实样本。
Day 4(§13-§16 四层治理):
- 第 2 层(跨线程协同):DB 写入与下载分离锁;OkHttp 连接池上限从 5 调到 32;Binder 调用加超时。
- 第 1 层(主线程减负):onCreate 改 App Startup + 异步 init。
- 第 3 层(监控前兆):WatchDog 阈值固定 2s,所有线程栈 + Binder 远端栈一并抓。
- 第 4 层(合规组件):BroadcastReceiver 改
goAsync()。
Day 5(§17 求证实验 思路验证):用流量回放工具构造 50% 大促流量灰度对比。
Day 6(上线复盘):第二次大促压测验证。
# 2.4 上线效果
| 指标 | 经验派 3 周后 | 方法派 6 天后 |
|---|---|---|
| 大促峰值 ANR 率 | 0.52% | 0.07% |
| iOS launch watchdog | 0.21% | 0.04% |
| 准 ANR 率(2s 阻塞) | 未监控 | 0.18% |
| 用户投诉量/日 | 8000+ | 240 |
| Google Play "ANR 标签" | 已挂 | 移除 |
核心洞察:经验派 3 周折腾全部错在"看主线程栈"。off-CPU 类 ANR 70%,主线程栈是无辜的,真因在其他线程或其他进程。 这正是 §17.4 实验 "配合所有线程栈"的硬证据。
# 2.5 案例如何串起本文
- §03 ANR 本质 ▶▶ ANR 不是"主线程卡 5 秒",而是"事件等待超时"。
- §04 触发模型 ▶▶ 70% 是 off-CPU 类 ANR,符合反直觉问题 ③。
- §07-§11 各类全链路 ▶▶ 输入/Service/Broadcast/CP/iOS Watchdog 各自的物理原理。
- §06 决策树 ▶▶ 案例的 4 种模式分别走不同分支。
- §17 求证实验 ▶▶ §17.2 抓栈时机、§17.4 所有线程栈都对应案例的归因路径。
- §13-§16 四层治理 ▶▶ "主线程减负→跨线程协同→监控前兆→合规组件"四层正是案例落地路径。
# 03.ANR 本质定义
# 3.1 ANR 的物理本质
ANR = 系统判定"应用主线程长时间不响应输入或关键事件",触发的兜底处理机制。
三个不可商量的物理约束:
约束一:ANR 的判定权在系统而非应用
应用代码无法控制"是否触发 ANR"——系统按固定规则监控主线程响应。这是用户体验的"系统防线":保证系统层面不被任何单一应用拖死。
约束二:ANR 触发依赖"事件超时",不是单纯"卡顿时间"
很多人误以为"主线程卡 5 秒就 ANR"。实际上:
- 必须有事件未处理:触摸 / Vsync / Service / Broadcast 进入队列后,5s 内未处理才触发。
- 静态卡顿不一定 ANR:用户没操作 / 没系统事件,主线程卡再久也不一定 ANR。
- 只看队首消息超时:是"队首消息没被取出处理超过 5 秒",不是"任何消息排队超 5 秒"。
约束三:ANR 是"卡顿"的极端形态
主线程响应延迟(用户感知):
< 100ms 无感
100ms-500ms 轻微卡顿
500ms-2s 明显卡顿
2s-5s 严重卡顿(用户可能反馈"卡死")
≥ 5s 系统判定 ANR ← 兜底
2
3
4
5
6
ANR 是已经卡了 5 秒的卡顿。治理 ANR 的根本是治理卡顿(详见 卷三·03)。
探索性思考:为什么"ANR 是兜底而非告警"? 因为 OS 的设计哲学是**"保护用户体验底线"——任何应用都不应该让整个系统看起来卡死。ANR 触发时系统已经放弃指望应用自己恢复——直接弹框让用户选择"等还是杀"。这是单方面的"惩罚"——应用层无权决定。所以应用的策略只能是"不要走到 ANR"**——通过监控前兆 + 兜底逃生让系统永远不需要触发兜底。这就是 §02 案例 方法派"WatchDog 2s 阈值"的根本动机:系统 5s 才看,我们 2s 就看——比系统快一步永远是赢家。
# 3.2 ANR 的现象与代价
ANR 是用户感知最严重的"非崩溃"性能问题:
- 应用无响应弹窗:Android 系统弹"应用未响应",让用户选择"等待 / 关闭"。
- 静默被杀:Android 11+ 部分场景直接杀进程不弹框。
- iOS Watchdog:启动 / 后台超时,iOS 直接杀进程(无弹窗,类崩溃)。
- 嵌入式看门狗复位:硬件看门狗超时,系统重启。
- Web 页面冻结:浏览器提示"页面无响应"。
业务代价:
- 头部 App 数据:ANR 率每降 0.1%,留存 +0.5%。
- iOS Watchdog 直接计入崩溃率,对应用商店评级影响大。
- Google Play Console 把 ANR 率 > 0.47% 标记为"过度的应用未响应",影响曝光。
▶▶ 回扣 §02 案例:电商大促 ANR 率从 0.08% 飙到 0.52%,直接踩中 Google Play 的 0.47% 红线被打标签,应用商店推荐位被降权——这正是"业务代价"在真实世界的钱袋打击。单日 2300 万订单流失说明 ANR 治理不是"工程美学",而是直接 ROI。
# 3.3 度量准则
按 卷零·02 §3 指标体系:
资源视角(USE):
| 指标 | 含义 | 阈值参考 |
|---|---|---|
| 主线程繁忙率 | 主线程消息处理占比 | < 80% |
| ANR 错误数 | 单位时间 ANR 频次 | < 1/天 |
| 主线程阻塞 | > 5s 阻塞次数 | = 0 |
请求视角(RED):
| 指标 | 含义 | 阈值参考 |
|---|---|---|
| ANR 率 | ANR / 总会话 | < 0.1% |
| 准 ANR 率 | 主线程阻塞 ≥ 2s 但未触发 ANR | < 0.5% |
| 主线程消息 P99 | 单条消息时长 | < 500ms |
用户感知(APDEX):
- Satisfied:无 ANR、无显著主线程阻塞
- Tolerating:偶发轻度阻塞(< 2s)
- Frustrated:ANR 弹窗 / 进程被杀
# 3.4 行业基准与目标
| 平台 | ANR / Watchdog 阈值 | 行业基准 |
|---|---|---|
| Android(普通) | 输入事件 5s / Service 20s | ANR 率 < 0.1% |
| iOS 启动 | 20s | Watchdog crash < 0.05% |
| iOS 后台 | 30s(前台→后台过渡时) | 通常无 |
| Web | 浏览器 ~10-30s(不一) | 罕见但存在 |
| 嵌入式 | 厂商定(通常几秒) | 0%(必须) |
# 3.5 8 个反直觉问题
带着这些问题阅读:
- ANR 5 秒阈值是固定的吗?
- 子线程死锁会触发 ANR 吗?
- 主线程没卡顿,怎么也会 ANR?
- iOS 没 ANR,是不是没 Watchdog 问题?
- ANR 弹出时主线程在做什么?能从堆栈准确归因吗?
- ANR Trace 文件保存在哪?怎么读?
- 后台进程也会 ANR 吗?
- ANR 一定是代码问题吗?
# 04.触发条件原理
本节回答四个根本问题:①ANR 触发条件有哪几类?②on-CPU vs off-CPU vs 系统抢占的物理差异?③为什么 70% ANR 是 off-CPU?④跨平台触发模型有何同构?
# 4.1 ANR 触发的四大类
ANR 触发 = 任一路径成立
┌────────────────────────────────────────────────┐
│ A. 输入事件超时(最常见,90%+) │
│ InputDispatcher 5 秒内未收到应用响应 │
│ 根因:主线程长任务、IO、锁、IPC │
├────────────────────────────────────────────────┤
│ B. Service 超时 │
│ foreground 20s / background 200s 未完成 │
│ 根因:Service 内同步重任务 │
├────────────────────────────────────────────────┤
│ C. Broadcast 超时 │
│ onReceive 同步执行 > 60s(前台)/ 10s(后台) │
│ 根因:Broadcast 中做长任务 │
├────────────────────────────────────────────────┤
│ D. ContentProvider 发布超时 │
│ publish 超过 10s │
│ 根因:CP onCreate 太重 │
└────────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
关键认知:A 类占线上 ANR 90%+。其他类型相对少见但严重。
# 4.2 主线程"卡"的物理来源
┌──────────────────────────────────────────────┐
│ ① on-CPU 主导:主线程在烧 CPU │
│ 解析、计算、解码 │
├──────────────────────────────────────────────┤
│ ② off-CPU 主导:主线程在等 │
│ IO Wait、锁、Binder 调用、GC STW │
├──────────────────────────────────────────────┤
│ ③ 系统级阻塞:主线程被压制 │
│ 高负载、降频、其他进程抢占 │
└──────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
反直觉问题 ③:主线程没卡也能 ANR——答案是 ② 类(off-CPU),主线程在等 IO / 锁,CPU 占用低但事件没人处理。
▶▶ 回扣 §02 案例:电商大促的 70% ANR(模式 A/B/C)都是 ② 类 off-CPU——栈顶看到
nativePollOnce/Binder.transact/Object.wait都是"等",CPU 占用低,但用户体感是"卡死"。经验派 3 周折腾全部基于"主线程没在干活就不是我们的问题"的错误推理。
# 4.3 为什么 70% ANR 是 off-CPU 类
理论原因:现代手机 CPU 性能足够强(6-8 核 / 2-3GHz)——纯计算任务在主线程跑 5 秒非常少见。真正的 5 秒卡,绝大多数是"等":
- 等锁:DB 写锁、Java synchronized、ReentrantLock。
- 等 IO:磁盘读写(SP/数据库/文件)。
- 等 Binder:跨进程调用(system_server 慢)。
- 等网络:主线程同步等子线程网络结果。
- 等 GC:Stop-The-World GC(少见但严重)。
工程意义:ANR 治理的重点是"等待治理",不是"计算优化"。这就是 §14 治理二层协同 的全部命题。
# 4.4 跨平台同构原理
任何带有"用户体验底线"的系统都需要保证:单一应用不能因为自身问题让整个系统看起来卡死。所以所有平台都演化出"无响应监督"机制:
通用无响应监督模型:
[系统监督方] ──▶ [向应用发探测/事件]
│
▼
[应用 N 秒内响应了吗?]
│
┌────┴────┐
▼ ▼
是 → 正常 否 → 触发兜底
│
▼
弹框 / 杀进程 / 重启
2
3
4
5
6
7
8
9
10
11
12
13
每个平台都必须有:
| 抽象组件 | 解决什么问题 |
|---|---|
| 监督方 | 谁在判定"无响应" |
| 探测机制 | 用什么衡量"响应" |
| 超时阈值 | 多久算"无响应" |
| 兜底动作 | 触发后做什么 |
跨平台术语对照
| 通用术语 | Android | iOS | Web | 嵌入式 |
|---|---|---|---|---|
| 监督方 | system_server / InputDispatcher | XNU watchdog | 浏览器主进程 | 硬件看门狗 |
| 探测机制 | 事件队列消化 | 启动 / 转后台超时 | 主线程响应 | "喂狗"间隔 |
| 阈值 | 输入 5s / Service 20s | 启动 20s / 后台 30s | 浏览器配置 | 厂商定 |
| 兜底动作 | 弹框 / Force Kill | 直接 SIGKILL | 弹框"未响应" | 系统复位 |
# 4.5 平台差异点矩阵
| 维度 | Android | iOS | Web | 嵌入式 |
|---|---|---|---|---|
| 弹框还是直接杀 | 弹框(早期)/ 静默杀(11+) | 直接杀 | 弹框 | 复位 |
| 阈值多样性 | 多种事件不同阈值 | 统一 20-30s | 浏览器决定 | 厂商定 |
| 现场 trace | /data/anr/traces.txt | crash log + MetricKit | DevTools | 自定义 |
| 用户感知 | 系统弹框 | "突然退出" | "无响应"按钮 | 系统重启 |
| 后台是否触发 | 是(Service / Broadcast) | 是(后台过渡) | 罕见 | 看门狗永远在 |
后续 §07-§11 各类 ANR 全链路章节会逐一展开。
探索性思考:为什么 iOS 没有"运行时 ANR"概念? 因为 iOS 的设计哲学是**"严控后台 + 主线程保护"**——应用切到后台 30s 强制挂起,主线程不能做长任务(CPU 监控会触发降级)。所以 iOS 的"无响应"主要发生在启动期(dyld + AppDelegate)和后台过渡期。Android 因为允许更自由的后台执行,反而需要更复杂的 ANR 机制兜底。这是"自由 vs 严格"两种设计哲学的不同后果——iOS 用户感知"应用突然消失",Android 用户看到"应用未响应弹框"——前者更优雅但应用开发者更受限。
# 05.度量与采集
# 5.1 三类捕获方案
ANR 的采集方案有三类,区别在于在 ANR 发生的"前 / 中 / 后"哪一段下钩子:
平稳期 ──▶ 主线程开始卡 ──▶ ANR 触发 ──▶ 系统兜底
│ │ │
▼ ▼ ▼
① 主线程消息监控 ② 准 ANR 预警 ③ ANR 现场抓取
(LooperPrinter) (WatchDog) (UncaughtException +
系统 trace 文件)
2
3
4
5
6
① 主线程消息监控(与卡顿同源)
- 核心原理:在主线程消息分发前后埋点,监控每条消息的处理耗时。
- 物理本质:把"主线程卡顿"作为 ANR 前兆,提前 5 秒发现问题。
- 适用场景:所有应用必备。配合 ② ③ 形成完整链路。
② 准 ANR 预警(WatchDog 心跳)
- 核心原理:在子线程定期 ping 主线程,超过阈值即视为"准 ANR",主动抓栈。
- 物理本质:在系统的 5 秒兜底之前,主动以更短阈值(如 2-3 秒)抓现场。
- 为什么这样有效:系统 ANR 弹框前抓栈最准确(主线程仍在卡)。阈值可调,可以分级(2s / 3s / 5s)。抓到的栈比
/data/anr/traces.txt时机更早,更接近"卡的根源"。 - 适用场景:与 ① 配合,是 ANR 治理的最佳工具。
③ ANR 现场抓取(系统 trace + UncaughtException)
- 核心原理:ANR 触发后,系统会在
/data/anr/traces.txt写入所有进程主线程栈,应用监听这个事件 + 读取数据上报。 - 物理本质:ANR 是"现场"问题,必须在系统兜底前后留下痕迹。
- 局限根源:
/data/anr/traces.txt在 Android Q+ 应用通常无权限读;iOS 启动 watchdog 直接 SIGKILL,几乎无法抓;MetricKit 数据延迟 24h。 - 适用场景:必备的兜底,但要做好"现场抓取失败"的兜底。
三种方案的总览
| 方案 | 钩子位置 | 数据粒度 | 性能开销 | 跨端通用性 | 线上可用 | 主要局限 |
|---|---|---|---|---|---|---|
| ① 主线程消息监控 | 消息边界 | 消息级 | 低 | 高 | ✅ | 看不到 next() 内部 |
| ② 准 ANR 预警 | 子线程心跳 | 阈值级 | 低 | 极高 | ✅ | 短卡顿漏报 |
| ③ ANR 现场抓取 | 异常 / 系统事件 | 全栈 | 中 | 中 | ⚠️ 受系统限制 | 易失败 |
方案的"组合定律":① 看趋势 + ② 主动抓 + ③ 兜底。核心是 ②:在系统兜底前抓栈最准。
探索性思考:为什么"WatchDog 子线程心跳"是 ANR 治理的核心武器? 因为它不依赖系统——纯应用层实现,所有平台都能用。核心思想:子线程定期 post 任务到主线程,如果主线程没在 N 秒内执行这个任务,说明主线程被卡。这种"心跳探活"的思想在分布式系统里也常用(服务健康检查)。WatchDog 把这种思想下放到应用内——子线程是"健康检查者",主线程是"被检查者"。这是 ANR 治理从"被动接通知"到"主动探测"的范式转变。
# 5.2 各方案的可见盲区
| 现象 | 方案 ① | 方案 ② | 方案 ③ |
|---|---|---|---|
| 主线程长任务 | ✅ | ✅ | ✅ |
| 主线程被切出 CPU(off-CPU) | 部分 | ✅ | ✅ |
| InputDispatcher 内部阻塞 | ❌ | ✅(如果调度延迟高) | ✅ |
| 系统级抢占 | ❌ | 部分 | ✅ |
| iOS 启动 watchdog | N/A | 部分(启动栈) | MetricKit |
盲区一:iOS 启动 watchdog:发生时进程被 SIGKILL,应用代码已死。唯一手段:MetricKit 下次启动报告。
盲区二:传统 ANR trace 权限受限:Android Q+ 普通应用不能读 traces.txt。只能靠 ② 提前抓。
# 5.3 跨平台采集对照表
| 维度 | Android | iOS | Web | 嵌入式 |
|---|---|---|---|---|
| 主线程消息 | LooperPrinter | RunLoop Observer | longtask Performance Entry | 自定义 |
| WatchDog | 自实现(HandlerThread + check) | 自实现(GCD 子线程) | 自实现(Worker) | RTOS 实现 |
| 系统 trace | /data/anr/traces.txt(受限) | MetricKit MXLaunchHangDiagnostic | DevTools timeline | 串口日志 |
| 历史 ANR | getHistoricalProcessExitReasons | MetricKit | N/A | 日志系统 |
| 用户感知"无响应" | 系统弹框 | App 突然退出 | 浏览器弹框 | 系统重启 |
# 5.4 数据可信度评估
| 数据 | 可信度 | 偏差来源 |
|---|---|---|
| 主线程消息耗时 | 高(< 1ms 误差) | 几乎无 |
| WatchDog 抓栈时机 | 中 | 抓栈时刻可能已脱离卡的真因 |
| ANR trace 文件 | 高(有权限时) | 系统直接 dump |
| MetricKit 报告 | 高 | 延迟 24h 但准确 |
# 06.归因决策树
# 6.1 ANR 归因决策树
ANR 触发
│
├── 类型 A. 输入事件超时(最常见,90%+) ──▶ 主线程卡顿归因
│ ├─ on-CPU 卡 → 业务计算 / 解析
│ ├─ off-CPU 卡 → IO / 锁 / Binder / GC
│ └─ 系统抢占 → 看 CPU 总负载
│
├── 类型 B. Service 超时 ──────────────────▶ Service 内任务过重
│ ├─ onCreate 同步重任务
│ └─ JobIntentService 已废弃,迁 WorkManager
│
├── 类型 C. Broadcast 超时 ────────────────▶ Broadcast onReceive 太重
│ ├─ 改 goAsync()
│ └─ 改用 WorkManager
│
└── 类型 D. ContentProvider 发布超时 ──────▶ CP onCreate 重
├─ 启动期串行(详见卷四·01)
└─ 用 App Startup 改造
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
反直觉问题 ⑧ 答案:ANR 不一定是代码问题。可能是设备低电量降频 + 后台进程抢 CPU让主线程消息处理慢。看 trace 中"是不是单一线程都慢"可以判断。
▶▶ 回扣 §02 案例:电商大促的 4 种模式精准映射到决策树——
- 模式 A(DB 锁竞争 42%) → A 类输入超时 → off-CPU 卡(锁)→ 主线程等 DB 写入线程释放锁。
- 模式 B(Binder 远程慢 28%) → A 类 → off-CPU 卡(Binder)→ system_server 大促时变慢,这正是反直觉问题 ⑧ 的真实案例:不是 App 代码的错,但 App 必须做超时兜底。
- 模式 C(OkHttp 连接池 18%) → A 类 → off-CPU 卡(锁)→ 自家代码的连接池配置不当。
- 模式 D(onCreate 串行 SDK 12%) → A 类 → on-CPU 卡 → 启动期同步任务。
同一次大促事件包含 4 种归因路径——一刀切优化必然失败。
# 6.2 现场快照分析
主线程栈的几种典型形态:
| 栈顶 | 含义 | 优化方向 |
|---|---|---|
Object.wait / LockSupport.park | 等锁 | 找谁持锁,缩粒度 |
nativePollOnce | 主线程在 Looper 等消息(正常) | 看其他线程是否阻塞了入队 |
epoll_wait / read | IO 等待 | 异步化 |
Binder.transact | 跨进程调用 | 远端慢 / 异步化 |
SQLiteDatabase.query | DB 同步 | 异步化 / 缓存 |
CalcEngine.run 等业务函数 | 业务计算 | 算法优化 / 异步 |
配合所有线程栈:单看主线程栈不够,要看:
- 持锁的线程栈(找出谁持锁)
- 高 CPU 占用的线程(可能是 GC)
- Binder 远端栈(哪个进程慢)
# 6.3 系统级阻塞归因
主线程"看似没忙"但 ANR 的常见根因:
- CPU 高负载:其他进程占满 CPU,主线程被调度延迟。
- 温度降频:高温降频,主线程任务变慢。
- 后台 GC 风暴:其他进程触发系统 GC,影响所有进程。
- 设备低电量:CPU 降频,原本能跑完的任务超时。
诊断方法:看系统级 trace(Perfetto / Systrace)的 sched 视图,分析"主线程线程被切出 CPU 的时长"。
# 6.4 ANR 前兆识别
为什么"前兆"比 ANR 更有价值:ANR 触发时已经晚了。前兆给的是"问题正在发生中"的预警,可以主动归因。
典型前兆:
| 前兆 | 信号 | 应对 |
|---|---|---|
| 准 ANR(2s 阻塞) | WatchDog 抓栈 | 上报详细栈 |
| 主线程消息 P99 > 1s | LooperPrinter 监控 | 检查是否有大任务 |
| 主线程帧时长 > 700ms | 帧回调监控 | 已是冻屏 |
| 系统级 CPU 持续 > 80% | 资源监控 | 检查后台任务 |
探索性思考:为什么"前兆 → ANR" 时间分布如此分散(30s 到 5 分钟)? 因为不同根因的"恶化速度"不同:
- DB 锁累积 → 慢慢恶化(几分钟)。
- 网络突发慢 → 突然爆发(30 秒内)。
- GC 风暴 → 阶段性(每 10-30 秒一次)。
前兆监控的意义不是"精准预测",而是"早期警报"——只要有任何一种信号,就触发现场抓取。误报率 = 提前介入 vs 漏报率 = 错过黄金窗口——前者代价小(多抓一次栈),后者代价大(无法归因)。所以前兆监控应当宁可误报不漏报。
# 07.输入事件全链路 ⭐
本章把输入事件 ANR 从"用户触摸"一路拆到"系统弹框",回答:InputDispatcher 怎么发送事件 / 应用何时 ack / 5s 超时如何计算。
# 7.1 输入事件的物理本质
用户触摸屏幕
│
▼
触摸驱动产生事件 → /dev/input/event*
│
▼
system_server 的 InputReader 读取
│
▼
InputDispatcher 分发到目标应用
│
▼
应用 InputEventReceiver 收到事件
│
▼
主线程 InputManagerService.handleInputEvent
│
▼
Activity / View 处理
│
▼
应用 finishInputEvent ack 给 system_server
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
关键事实:
- 事件分发是 IPC 异步——system_server 发,应用收,应用要 ack。
- 5s 超时计时点是"system_server 发出事件" —— 不是"应用收到事件"。
- 应用必须在 5s 内 ack —— 即使没处理完也要先 ack,再异步处理。
# 7.2 输入事件 ANR 的触发链路
t=0: system_server 发出触摸事件 → 启动 5s 超时定时器
│
▼
t=10ms: 应用收到事件 → 入主线程消息队列
│
▼
t=11ms: 主线程开始处理事件
│
├─ 正常路径:很快处理完 → ack → 取消定时器
│
└─ 异常路径:主线程卡 5s+
│
▼
t=5s: system_server 定时器触发
│
▼
AMS.appNotResponding()
│
├─ 给应用进程发 SIGQUIT → ART dump 栈到 /data/anr/traces.txt
│
├─ 抓取 CPU 使用率快照
│
▼
决策:弹"应用无响应"对话框 / 直接 kill / 静默记录
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
两个隐藏陷阱:
① 5s 计时是"系统发出"为零点:如果系统发出后还在系统层(如 Binder 队列)排队 1s,应用实际只有 4s。所以应用监控阈值应低于 5s——这是 §17.1 实验 的根因。
② 多个事件累积超时:如果连续来 10 个事件,主线程处理第 1 个慢导致后续都没处理,超时按"最早未处理事件"算。
# 7.3 应用内的事件分发链
ViewRootImpl.dispatchInputEvent
│
▼
View.dispatchTouchEvent(视图树遍历)
│
▼
View.onTouchEvent(叶子 View 处理)
│
▼
业务逻辑(onClick / onLongClick)
│
▼
返回 → ack
2
3
4
5
6
7
8
9
10
11
12
13
关键事实:视图树遍历也是主线程做的——如果视图层级很深(> 20 层),单次 dispatch 可能 50-100ms。深层视图 + 复杂 onClick 业务 = ANR 高发。
# 7.4 输入事件 ANR 的应对
| 场景 | 应对 |
|---|---|
| 主线程做长任务 | 异步化(§13) |
| 视图层级太深 | 重构布局(卷三·05) |
| onClick 中做 IO | 异步化 |
| 主线程同步等子线程 | 改异步 + 超时(§14.2) |
探索性思考:为什么"输入事件 ANR"占 90%+? 因为输入是用户与应用的主要交互通道——其他事件(Service / Broadcast / CP)多是后台或一次性任务。只要用户在用应用,就在持续发输入事件。所以 90% ANR 都是输入超时。这反过来说明输入路径是 ANR 治理的主战场——任何放在主线程的"重活"迟早会被某次输入事件超时撞上。这就是 §13 治理一层减负 是基础的根因。
# 08.Service 全链路 ⭐
本章把 Service ANR 从"启动 Intent"一路拆到"超时 kill",回答:为何 foreground 20s vs background 200s / Service 该用什么替代。
# 8.1 Service 的生命周期
startService(intent)
│
▼
AMS 创建 Service 进程(如不存在)
│
▼
Service.onCreate(一次性)
│
▼
Service.onStartCommand(每次 startService 都调用)
│
▼
返回(可选 START_STICKY 等)
│
▼
Service 持续运行直到 stopSelf
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ANR 触发点:
- onCreate / onStartCommand 同步执行超时:foreground 20s / background 200s。
- onBind 同步执行超时:同上。
- onDestroy 同步执行超时:少见但存在。
# 8.2 为何 foreground vs background 阈值不同
foreground Service(用户感知中):
- 用户主动启动(如音乐播放)
- 阈值 20s(人类感知忍耐上限)
background Service(用户不感知):
- 后台同步任务
- 阈值 200s(10× 宽松)
2
3
4
5
6
7
这反映了 OS 的优先级哲学——前台任务必须快,后台任务可以慢。
# 8.3 Service 的现代替代
Android 8+ 严格限制 background Service。官方推荐:
| 旧方案 | 新方案 | 优势 |
|---|---|---|
| IntentService | WorkManager | 自动重试、约束调度、合规 |
| JobIntentService | WorkManager | 同上 |
| 后台 Service + WakeLock | WorkManager + Foreground Service | 系统支持、节电 |
| BroadcastReceiver + Service | WorkManager 直接调度 | 省去中间层 |
WorkManager 的 ANR 优势:
- 不在主线程执行(默认子线程)。
- 系统统一调度(不会突发并发)。
- 失败自动重试(不需自实现)。
# 8.4 Service ANR 的现场归因
/data/anr/traces.txt(系统)
│
▼
找到目标进程
│
▼
主线程栈中应该有 Service.onCreate / onStartCommand
│
▼
栈顶是什么?
├─ IO/DB → 异步化
├─ 网络同步 → 改 WorkManager
├─ 锁等待 → 锁治理
└─ 业务计算 → 切片 / 算法优化
2
3
4
5
6
7
8
9
10
11
12
13
14
探索性思考:为什么"Service ANR"在新应用中越来越少? 因为 Android 8+ 限制 background Service 后,很多旧用法已不可行——开发者被迫用 WorkManager 等现代 API。这是 OS 通过限制驱动应用现代化的成功案例。反过来说,仍然遇到 Service ANR 的应用通常是"老代码遗留"——代码可能是 Android 7 时代写的,没有跟上现代 API。ANR 治理的一部分是"代码考古 + 现代化"。
# 09.Broadcast 全链路 ⭐
本章把 Broadcast ANR 从"sendBroadcast"一路拆到"goAsync()",回答:为何 onReceive 同步执行有 10s/60s 上限 / goAsync 怎么工作 / 为何不能解决一切。
# 9.1 Broadcast 的分发机制
sendBroadcast(intent)
│
▼
AMS 查找匹配的 BroadcastReceiver(manifest + dynamic)
│
▼
按优先级排序(priority 属性)
│
▼
逐个调用 onReceive(同步串行)
│
▼
全部完成 → 广播结束
2
3
4
5
6
7
8
9
10
11
12
13
关键事实:
- Broadcast 默认串行——一个 Receiver 慢会拖累所有后续。
- onReceive 默认在主线程——同步阻塞即 ANR。
- 超时阈值:前台进程 60s,后台进程 10s。
# 9.2 onReceive 同步陷阱
// ❌ 反例:主线程同步做 IO
@Override
public void onReceive(Context context, Intent intent) {
String data = readFromDisk(); // 主线程 IO
db.insert(data); // 主线程 DB
// 超过 10s = ANR
}
2
3
4
5
6
7
// ✅ 正例:goAsync() 异步
@Override
public void onReceive(Context context, Intent intent) {
PendingResult result = goAsync();
executor.submit(() -> {
try { handleHeavyTask(intent); }
finally { result.finish(); }
});
}
2
3
4
5
6
7
8
9
# 9.3 goAsync 的工作原理
onReceive 调用 goAsync()
│
▼
返回 PendingResult,告诉 system_server "我还在处理"
│
▼
onReceive 直接返回(避免主线程阻塞)
│
▼
后台线程异步处理
│
▼
完成 → result.finish() 通知系统
2
3
4
5
6
7
8
9
10
11
12
13
收益:onReceive 同步执行 0ms,主线程不阻塞。
局限:
- goAsync 仍有上限:10s(前台)/ 60s(后台)后系统强制完成。
- 不能用于真正的长任务——超长任务必须用 WorkManager。
# 9.4 Broadcast 的现代替代
长任务:
❌ Broadcast + 主线程 IO
✅ Broadcast + goAsync (短任务,<10s)
✅ Broadcast + WorkManager (长任务)
✅ 不用 Broadcast,直接业务事件 + LocalBroadcastManager / EventBus
2
3
4
5
LocalBroadcastManager 在应用内 Broadcast 场景:
- 不出进程,不需 IPC。
- 不会 ANR。
- 但 androidx 已废弃(推荐用 LiveData / Flow)。
探索性思考:为什么 Android 把 Broadcast 设计成主线程串行? 因为 Broadcast 设计于早期(Android 1.0),那时移动开发还没"主线程不能做重活"的共识。Broadcast 的串行设计是为了"严格按优先级执行"——但代价是任何一个 Receiver 慢都会拖累所有。现代视角看这是设计缺陷——但已在生态中根深蒂固,无法改。所以 Android 引入 goAsync + WorkManager 等"补丁"——而不是改 Broadcast 本身。这是软件工程的现实——很多设计决策是历史包袱,只能渐进改善。
# 10.ContentProvider 全链路 ⭐
本章把 CP ANR 从"应用启动"一路拆到"App Startup",回答:为何 CP onCreate 在 Application 之前 / 为何会成为 ANR 高发地 / 怎么治。
# 10.1 ContentProvider 的启动时机
应用进程被 Zygote fork
│
▼
ActivityThread.handleBindApplication
│
├─ installContentProviders() ← 这里!
│ │
│ ├─ 加载所有 manifest 中声明的 Provider
│ ├─ 依次调用 Provider.onCreate(主线程串行)
│ └─ 每个超 10s = ANR
│
▼
Application.onCreate (之后才执行)
│
▼
Activity 创建 / 显示
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
关键事实:
- CP onCreate 在 Application.onCreate 之前——很多 SDK 利用这点"偷启动"。
- CP onCreate 串行执行——一个慢拖所有。
- CP onCreate 必须在主线程——不能异步化(系统调用入口固定)。
# 10.2 SDK 偷启动的陷阱
<!-- 各种 SDK 在 manifest 中注册自己的 Provider -->
<provider
android:name="com.google.firebase.provider.FirebaseInitProvider"
android:authorities="${applicationId}.firebaseinitprovider"
android:exported="false" />
<provider
android:name="com.facebook.FacebookContentProvider"
... />
<!-- 几十个 SDK 各自注册 -->
2
3
4
5
6
7
8
9
10
11
这导致:
- 应用启动时依次跑 N 个 Provider.onCreate。
- 每个 50-200ms,N 个累积起来 1-3s。
- 如果某个 Provider 内部做网络/IO,可能 10s+ 触发 ANR。
# 10.3 App Startup 的解决方案
卷四·01 §14.3 已述。核心思想:
- 删除所有第三方 SDK 的 Provider。
- 用 androidx.startup.InitializationProvider 统一管理。
- 内部用 DAG 调度初始化任务。
收益:
- N 个 Provider → 1 个 Provider,启动更快。
- 显式调度 → 可预测、可控。
- 减少 D 类 ANR 风险。
# 10.4 CP ANR 的现场归因
/data/anr/traces.txt 显示主线程在:
- ContentProviderClient.publish
- ContentProvider.onCreate(具体哪个 Provider)
│
▼
该 Provider 的 onCreate 实现:
- SDK 内部做了什么慢操作?
- 是否能延迟到 Application.onCreate 后?
- 是否能改 App Startup 集成?
2
3
4
5
6
7
8
9
探索性思考:为什么 Google 不彻底废弃 ContentProvider? 因为它是应用间数据共享的核心机制——通讯录、媒体库、文件等系统服务都用 CP 暴露数据。CP 本身没问题,问题是 SDK 滥用了"自动启动"特性。应对:Google 没废弃 CP,而是引入 App Startup 提供统一的"自动启动"接口——让 SDK 集成到 InitializationProvider,避免各自注册。这是"在有问题的设计上加新设计来规范化" ——典型的渐进式架构演进。
# 11.iOS Watchdog 全链路 ⭐
本章把 iOS Watchdog 从"启动超时"一路拆到"MetricKit",回答:为何 iOS 没有"运行时 ANR" / 启动 watchdog 怎么工作 / 怎么监控。
# 11.1 iOS Watchdog 的物理本质
iOS 没有 Android 的"运行时 ANR",只有几个特定场景的 watchdog:
┌──────────────────────────────────────────┐
│ 启动 Watchdog(最常见) │
│ 阈值:20s(前台启动) │
│ 触发:dyld + main + AppDelegate 总耗时 │
│ 结果:直接 SIGKILL(无弹框) │
├──────────────────────────────────────────┤
│ 后台过渡 Watchdog │
│ 阈值:5s(前台→后台)/ 30s(后台启动) │
│ 触发:applicationDidEnterBackground 超时 │
│ 结果:SIGKILL │
├──────────────────────────────────────────┤
│ Background Task Watchdog │
│ 阈值:30s(标准)/ 几小时(特殊任务) │
│ 触发:beginBackgroundTask 超时 │
│ 结果:SIGKILL │
└──────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
关键差异 vs Android ANR:
- 直接杀进程,无弹框——用户感知是"应用突然消失"。
- 不可捕获——SIGKILL 应用代码无法响应。
- 必须靠 MetricKit 事后报告——下次启动时拿到报告。
# 11.2 iOS 启动 Watchdog 触发链路
用户点击图标
│
▼
exec → dyld 链接所有动态库
│
▼
调用 main()
│
▼
UIApplicationMain → AppDelegate
│
▼
application:didFinishLaunchingWithOptions:
│
├─ 阶段总耗时 < 20s → 正常
│
└─ 总耗时 > 20s
│
▼
launchd 给进程发 SIGKILL(无前置通知)
│
▼
ReportCrash 写崩溃日志(特征码 0x8badf00d)
│
▼
下次启动时 MetricKit 在 24h 内异步报告
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
特征码 0x8badf00d("ate bad food"双关)= Watchdog 标识。
# 11.3 MetricKit 事后报告
import MetricKit
class MetricSubscriber: NSObject, MXMetricManagerSubscriber {
@available(iOS 14.0, *)
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
// 启动 hang
payload.applicationLaunchDiagnostics?.forEach { hang in
report("launch_hang", hang.callStackTree.jsonRepresentation())
}
}
}
}
MXMetricManager.shared.add(MetricSubscriber()) // didFinishLaunching 注册
2
3
4
5
6
7
8
9
10
11
12
13
14
MetricKit 的关键边界:
- 延迟 24 小时才能拿到。
- 数据已符号化——含 binary UUID/offset/symbol。
- 采样上报,不是逐次——线上统计可信,不能用于"逐崩溃归因"。
# 11.4 iOS 应用的 ANR 治理
| 场景 | 应对 |
|---|---|
| 启动 hang | 优化启动期(卷四·01) |
| 后台过渡 hang | applicationDidEnterBackground 内不做长任务 |
| MetricKit 监控 | 接入 + 上报 |
| 启动期同步初始化 | 移到 lazy 或 background |
探索性思考:为什么 iOS 选择"直接杀"而不是"弹框"? 因为 Apple 的设计哲学是**"用户优先 + 简单"**——弹框让用户做技术决策("等还是杀")违背了 iOS 的"非技术用户友好"原则。直接杀让用户感知是"应用挂了"——这是用户能理解的概念。反过来 Android 的弹框是"工程师视角" ——给用户选择权,但代价是用户困惑。两种设计哲学没有对错——只是优先级不同。Apple 优先用户体验简单性,Google 优先技术控制力。
# 12.跨端 ANR 对照
# 12.1 端到端流程对照表
| 阶段 | Android | iOS | Web |
|---|---|---|---|
| 监督方 | system_server / InputDispatcher | XNU watchdog | 浏览器主进程 |
| 触发条件 | 事件超时(输入 5s) | 启动 20s / 后台 30s | longtask / 主线程冻结 |
| 兜底动作 | 弹框 / Force Kill | SIGKILL(直接杀) | 弹框"未响应" |
| 现场获取 | /data/anr/traces.txt | MetricKit(24h 延迟) | DevTools |
| 应用监控 | LooperPrinter + WatchDog | RunLoop Observer + GCD | longtask Performance |
| 历史数据 | ApplicationExitInfo (R+) | MetricKit | N/A |
# 12.2 阈值对照
| 平台 | 输入事件 | Service | 启动 |
|---|---|---|---|
| Android(前台) | 5s | 20s | N/A(启动用 logcat Displayed) |
| Android(后台) | N/A | 200s | N/A |
| iOS | N/A(无运行时 ANR) | N/A | 20s |
| Web | 浏览器决定(10-30s) | N/A | N/A |
# 12.3 统一启示
- ANR 治理跨端通用:减少主线程任务 + 监控前兆 + 兜底逃生。
- iOS 严约束 = 启动福音:但启动慢直接 SIGKILL 比 Android ANR 弹框更严重。
- 现场抓取手段不同:Android 主动 + 系统 trace;iOS 只能靠 MetricKit。
- 应用层监控价值通用:WatchDog 子线程心跳在所有平台都能用。
# 13.治理一层减负
本节回答四个递进问题:①如何消灭主线程根因?②如何治理跨线程协同?③如何让兜底前可定位?④如何对四类组件分别施治? §13-§16 由浅入深四层。
# 13.1 一层命题
核心命题:on-CPU 类 ANR 的唯一根治手段是不在主线程做重活。第一层治理是"主线程减负"——这是基础,但不是全部(70% off-CPU 类还需要二层治理)。
层级特征:
- 改造成本:中(重构 IO/DB/网络调用)
- 收益:高(消除 30% on-CPU 类 ANR)
- 风险:中(异步化引入 lifecycle bug)
- 是否必做:所有应用
# 13.2 策略 1.1:主线程 IO/DB/网络的强制异步化
机理:主线程任何 IO/DB/网络都是潜在 ANR 源。Android StrictMode 直接禁用:
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyDeath() // Debug 包直接 crash 暴露
.build());
2
3
4
5
6
做法(异步化 DB 查询):
// 反例
val users = db.userDao().getAll() // 主线程同步,可能 ANR
// 正例:协程或 RxJava
viewModelScope.launch(Dispatchers.IO) {
val users = db.userDao().getAll()
withContext(Dispatchers.Main) { adapter.submitList(users) }
}
2
3
4
5
6
7
8
收益:§02 案例 模式 D(onCreate 串行 SDK 12%)直接消除。
边界:异步化后要处理"返回时页面已销毁"的 NPE 风险,用 lifecycle-aware 协程或 LiveData。
# 13.3 策略 1.2:onCreate / Application 启动期任务的分级调度
机理:Application.onCreate / Activity.onCreate 是 ANR 高发地,必须用 App Startup 重构。
做法:
class HeavyInitializer : Initializer<Unit> {
override fun create(context: Context) {
// 只放真正需要在启动前完成的任务
}
override fun dependencies(): List<Class<out Initializer<*>>> = listOf(LightInitializer::class.java)
}
2
3
4
5
6
收益:详见 卷四·01 §13。冷启动篇的金融 App 案例 4.2s→1.4s 直接降低 D 类 ANR。
边界:App Startup 仅适用于明确启动期任务;运行期 SDK init 用 Lazy 替代。
# 13.4 策略 1.3:长任务切片(避免单次 > 50ms)
机理:单次主线程消息超过 100ms 就开始有 ANR 风险。切成 < 8ms 片段。
做法:
class ChunkedTask {
private final Iterator<Item> it;
void process() {
long deadline = SystemClock.uptimeMillis() + 4; // 留余量
while (it.hasNext() && SystemClock.uptimeMillis() < deadline) {
handleOne(it.next());
}
if (it.hasNext()) Handler.post(this::process); // 下一帧继续
}
}
2
3
4
5
6
7
8
9
10
收益:单次任务最长不超 8ms,从根本上不会触发 ANR。
边界:切片不适合需原子性的任务(如事务);需要时用单独后台线程。
# 13.5 一层反思
探索性思考:为什么"主线程减负"在新应用中相对容易做到? 因为 Kotlin 协程 + lifecycle-aware 已经成为标准——
viewModelScope.launch(Dispatchers.IO)是默认姿势。新代码很少出现"主线程做 IO"的反例。真正的难点是"老代码改造"——很多业务逻辑已耦合在 UI 层,异步化要改一连串。所以 ANR 治理的"第一层"不是技术难度,而是历史代码改造意愿。
# 14.治理二层协同
# 14.1 二层命题
核心命题:§17.4 实验 证明 70% ANR 是 off-CPU 类。第二层治理是"跨线程协同"——这是 §02 案例 的主战场,也是经验派最容易忽视的地方。
层级特征:
- 改造成本:中(拆锁、加超时)
- 收益:极高(消除 70% off-CPU 类 ANR)
- 风险:中(拆锁引入并发 bug)
# 14.2 策略 2.1:锁内严禁 IO/网络/Binder
机理:§02 案例 模式 A 的根因——DB 写入线程持锁的同时做网络下载,主线程在 wait 锁等了 5s+。
做法:
// ❌ 反例:网络在锁内
synchronized (lock) {
db.write(data);
httpClient.upload(data);
}
// ✅ 正例:锁外做 IO,锁内只更新内存状态
String url = httpClient.upload(data); // 锁外
synchronized (lock) {
cachedUrl = url;
}
2
3
4
5
6
7
8
9
10
11
收益:模式 A(42%)几乎全消。
边界:拆锁可能引入幻读,需配合 CAS 或 ConcurrentHashMap。
# 14.3 策略 2.2:跨线程同步加超时
机理:§18.2 案例 的"主线程 wait 子线程 30s 网络"——子线程超时设短,主线程不能无限等。
做法:
// 主线程不要 future.get(),用 future.get(timeout)
try {
String r = future.get(800, TimeUnit.MILLISECONDS); // 800ms 上限
} catch (TimeoutException e) {
return defaultValue; // 兜底降级
}
2
3
4
5
6
收益:把"无限期等待"改为"最多 800ms",主线程永不会被等死。
边界:超时返回降级值,要确保业务正确性(如订单类不能降级)。
# 14.4 策略 2.3:连接池/线程池容量按峰值规划
机理:§02 案例 模式 C——OkHttp 默认 5 个连接池在大促被打满,请求排队。
做法:
ConnectionPool pool = new ConnectionPool(
32, // 大促按峰值 QPS / RTT 估算
5, TimeUnit.MINUTES
);
OkHttpClient client = new OkHttpClient.Builder().connectionPool(pool).build();
2
3
4
5
收益:模式 C(18%)几乎全消,且不会"突发流量打挂全局"。
边界:连接池 ≠ 越大越好,超过 100 会引发服务端连接耗尽。
# 14.5 策略 2.4:Binder 调用加超时与降级
机理:§02 案例 模式 B——system_server 大促时变慢,App Binder 同步等。
做法:
// AIDL 接口 oneway 化(异步)
interface IRemoteService {
oneway void notifyStateAsync(int state); // 不阻塞调用方
}
2
3
4
收益:模式 B(28%)大幅降低;oneway 调用不阻塞主线程。
边界:oneway 不能有返回值;需要返回值时用 RemoteCallback 异步回调。
# 14.6 二层反思
探索性思考:为什么"跨线程协同"是 ANR 治理的最大盲区? 因为它违反工程师的直觉——直觉认为"加锁是为了保护数据",但锁本身可能阻塞主线程。多数 ANR bug 的根因:
- 一开始 lock { db.write() } 是合理的(DB 是锁内)。
- 后来加了 lock { db.write(); upload() }("反正都在锁内")。
- upload() 是网络调用,可能 30s。
- 主线程下次 lock 时等 30s → ANR。
工程师视角"代码看起来没问题"——但 ANR 是"组合问题"。这就是为什么 ANR 比崩溃难调——崩溃看一行代码,ANR 看整个执行环境。根本应对:建立"锁内禁止 IO"的硬规则,Lint 检查。
# 15.治理三层监控
# 15.1 三层命题
核心命题:§17.3 实验 证明 85% ANR 有 30s+ 前兆。第三层治理是"让 ANR 在系统兜底前就能被定位归因"。
层级特征:
- 改造成本:低(基建型)
- 收益:长期(持续问题暴露)
- 风险:低
# 15.2 策略 3.1:WatchDog 2-3s 阈值 + 全栈抓取
机理:§17.2 抓栈时机 + §17.4 全栈范围。
做法:
class AnrWatchDog extends Thread {
private volatile boolean alive;
public void run() {
Handler main = new Handler(Looper.getMainLooper());
while (true) {
alive = false;
main.post(() -> alive = true);
SystemClock.sleep(2000); // 2s 阈值
if (!alive) {
// 抓所有线程栈
Map<Thread, StackTraceElement[]> all = Thread.getAllStackTraces();
// 触发 SIGQUIT dump
Process.sendSignal(Process.myPid(), Process.SIGNAL_QUIT);
report(all);
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
收益:能在系统 5s 兜底前 2-3s 抓到现场,匹配度 100%。
边界:WatchDog 自身约 0.3% CPU;阈值不能 < 1s(误报)。
# 15.3 策略 3.2:主线程消息 P99 持续监控
机理:§06.4 前兆识别 给出"主线程消息 P99 > 1s"是早期信号。
做法:用 LooperPrinter 详见 卷三·03 §3.1。
收益:§17.3 实验 证明 85% ANR 都有 30s+ 提前量,可以做到"问题在系统介入前就被告警"。
边界:LooperPrinter 性能损耗约 3%,建议采样 10% 用户。
# 15.4 策略 3.3:SIGQUIT Hook 与系统同源 dump
做法:核心是劫持 SIGQUIT 信号让 ART 自己 dump:
// JNI 层
struct sigaction sa;
sa.sa_sigaction = handle_sigquit;
sa.sa_flags = SA_SIGINFO;
sigaction(SIGQUIT, &sa, nullptr);
// handler 中调 art::Runtime::DumpForSigQuit() 等价物
2
3
4
5
6
收益:拿到与系统 /data/anr/traces.txt 一致的内容,无需 root。
边界:部分 OEM SELinux 策略限制,成功率 88-100%;需 fallback 到 getAllStackTraces。
# 15.5 策略 3.4:历史 ANR 补全统计
机理:Android 11+ getHistoricalProcessExitReasons 可补全"被静默杀"的 ANR。
做法:
val am = context.getSystemService(ActivityManager::class.java)
val reasons = am.getHistoricalProcessExitReasons(null, 0, 100)
reasons.filter { it.reason == ApplicationExitInfo.REASON_ANR }
.forEach { upload(it) }
2
3
4
收益:把 Android 11+ 静默杀的 ANR 也纳入统计,避免数据失真。
边界:仅 Android 11+;部分 OEM 实现不全。
# 15.6 三层反思
探索性思考:为什么"WatchDog"是被低估的 ANR 治理武器? 因为它不依赖系统、不需特殊权限、纯应用层实现——所有平台都能用,只要有"子线程 + 心跳"就行。多数团队认为"ANR 治理 = 等系统报告",但这恰恰是错的——系统报告时已经晚了 5 秒,且权限受限。WatchDog 让你在 2 秒就抓到现场——比系统快 2.5 倍。这是把"被动接通知"转变为"主动探活"的范式转变——同样思路在分布式系统、监控告警领域无处不在。
# 16.治理四层组件
# 16.1 四层命题
核心命题:A 类输入 ANR 占 90%+ 但不能忽视 B/C/D。第四层治理是用合规 API 替代不合规模式。
层级特征:
- 改造成本:中(重构组件使用方式)
- 收益:中-高(消除 B/C/D 类 ANR)
- 风险:低
# 16.2 策略 4.1:Broadcast 改 goAsync
做法:
@Override
public void onReceive(Context context, Intent intent) {
PendingResult result = goAsync();
executor.submit(() -> {
try { handleHeavyTask(intent); }
finally { result.finish(); }
});
}
2
3
4
5
6
7
8
收益:C 类 ANR 几乎消除。
边界:goAsync 仍有 10s(前台)/ 60s(后台)上限;超过应改 WorkManager。
# 16.3 策略 4.2:Service / 长任务改 WorkManager
做法:
val work = OneTimeWorkRequestBuilder<UploadWorker>()
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.build()
WorkManager.getInstance(context).enqueue(work)
2
3
4
收益:B 类 ANR 几乎消除;同时合规且可靠(WorkManager 处理重试)。
边界:WorkManager 调度延迟约 10s-15min(视约束),实时性差任务不适用。
# 16.4 策略 4.3:ContentProvider 启动期改 App Startup
做法:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup">
<meta-data android:name="com.example.MyInitializer"
android:value="androidx.startup" />
</provider>
2
3
4
5
6
收益:D 类 ANR 几乎消除;启动时序也更可控。
边界:必须改造所有第三方 SDK 集成方式,工作量大。
# 16.5 优先级判定(ROI)
| ROI | 优化项 | 收益 | 成本 | 风险 | 对应章节 |
|---|---|---|---|---|---|
| 极高 | WatchDog 2s + 全栈抓取 | 归因精度从 27% 升到 86% | 1 周 | 低 | §15.2 |
| 极高 | 主线程 IO/DB 异步化 | A 类 on-CPU ANR 大幅降 | 1-2 周 | 中 | §13.2 |
| 极高 | 锁内禁 IO/网络 | 模式 A 类几乎消除 | 1-2 周 | 中 | §14.2 |
| 高 | 跨线程同步加超时 | 长尾 ANR 治理 | 3-5 天 | 低 | §14.3 |
| 高 | SIGQUIT Hook | 现场归因质量提升 | 2 周 | 中 | §15.4 |
| 高 | Broadcast goAsync | C 类几乎消除 | 几天 | 低 | §16.2 |
| 高 | App Startup 替代多 CP | D 类几乎消除 | 1-2 周 | 中 | §16.4 |
| 中 | 连接池容量按峰值规划 | 模式 C 类消除 | 2-3 天 | 低 | §14.4 |
| 中 | Binder oneway 化 | 模式 B 类降低 | 1-2 周 | 中 | §14.5 |
| 中 | 历史 ANR 补全统计 | 数据准确性提升 | 几天 | 低 | §15.5 |
| 中 | iOS MetricKit 启动监控 | iOS watchdog 数据 | 几天 | 低 | §11.3 |
| 低 | 自实现 ANR trace 解析 | 微优化 | 高 | 中 | - |
避免反向收益:
- WatchDog 阈值过小(< 1s):频繁误报,引入噪声。
- 抓栈过频:影响性能,建议触发后单次抓取。
- 过度异步化:引入同步 bug;必须配合 lifecycle 检查。
- 改大监控阈值掩盖问题:§02 案例 第 3 周翻车的根因——系统不会陪你"自欺欺人"。
- goAsync 仍超时:要看到 goAsync 仍有 10s/60s 上限,长任务必须 WorkManager。
# 16.6 四层反思
探索性思考:为什么"组件治理"经常被推迟到最后? 因为它最不"性感"——主线程减负有"立竿见影"的效果,但 Broadcast 改 goAsync 看起来是"小修小补"。实际上 C/D 类 ANR 在某些应用占比可达 30-40%——只是不容易被注意到。应对:把组件治理纳入"代码现代化"的常规工作——每次重构相关代码时顺手改造。
# 17.求证实验
# 17.1 实验一:超时阈值精度
Step 1 — 原始观察
工程师都说"主线程卡 5 秒就 ANR",但实测发现有时卡 5 秒没 ANR,有时卡 4 秒就触发了。为什么?
Step 2 — 提出疑问
Android ANR 实际触发的时间分布是什么?是固定 5 秒还是有浮动?
Step 3 — 形成假设
H₁:5 秒是"事件入队后未处理 5 秒",不是"主线程卡 5 秒"。如果主线程卡时无新事件,可能 10 秒以上都没 ANR。 H₀:固定 5 秒。
Step 4 — 设计实验
| 项 | 配置 |
|---|---|
| 设备 | Pixel 6 |
| 场景 | (a) 主线程 sleep 不同时长,无任何输入;(b) sleep 期间发触摸事件 |
| sleep 时长 | 3s, 5s, 7s, 10s, 15s |
| 主指标 | 是否触发 ANR、距离 sleep 开始多久触发 |
Step 5 — 实测数据
| sleep 时长 | (a) 无输入 | (b) 触摸后 1s 进 sleep |
|---|---|---|
| 3s | 不触发 | 不触发 |
| 5s | 不触发 | 不触发(4s 仍在阈值内) |
| 7s | 不触发 | 在 sleep 后 6s 触发(5s + 队首消息排队 1s) |
| 10s | 不触发 | 在 sleep 后 6s 触发 |
| 15s | 不触发 | 同上 |
Step 6 — 验证 / 修正
- 无输入时,主线程卡 15s 也不触发 ANR(因为没有"未处理事件"超时)。
- 有触摸事件时,从事件入队开始计时,5 秒后触发。
Step 7 — 提炼结论
ANR 的 5 秒阈值是"事件入队到处理"的等待时长,不是"主线程卡"的时长。 静态卡顿(无事件)不一定 ANR。
工程意义:
- 监控不能只看"主线程卡多久",要配合"输入响应延迟"。
- WatchDog 监控更可靠(不依赖事件触发)。
- 用户没操作的卡顿仍然影响体验,监控不应漏掉。
Step 8 — 边界
- Service / Broadcast / CP 各有阈值,本结论仅针对输入。
- 不同 OEM ROM 阈值可能调整(如某些游戏机型放宽到 10s)。
▶▶ 回扣 §02 案例:经验派第 3 周"把 ANR 监控从 5s 改 10s"是典型的把"应用监控阈值"和"系统兜底阈值"搞混——改自己的监控只能让上报变少,系统该 5s 杀还是 5s 杀。
# 17.2 实验二:现场抓取时机
Step 1 — 原始观察
ANR 上报有时"栈跟实际原因对不上"。抓栈的时机是关键吗?
Step 2 — 提出疑问
WatchDog 在不同时机(2s / 3s / 5s)抓栈,得到的栈与"真实卡的根源"匹配度多大?
Step 3 — 形成假设
H₁:抓栈越早匹配度越高。5s 抓栈很可能已离开卡的根源。
Step 4 — 设计实验
| 项 | 配置 |
|---|---|
| 测试场景 | 构造已知卡顿:主线程做 6s 同步 IO,已知"卡的根源是 IO" |
| WatchDog 阈值 | 2s, 3s, 5s(在阈值时抓栈) |
| 主指标 | 抓到的栈是否含 IO 调用 |
Step 5 — 实测数据
| 阈值 | 含 IO 调用栈比例 |
|---|---|
| 2s | 100% |
| 3s | 100% |
| 5s | 87%(部分时刻 IO 已结束) |
| 7s | 45%(已离开 IO,进了下一段任务) |
Step 6-7 — 验证 / 结论
WatchDog 抓栈应在 2-3s 进行(系统 ANR 5s 之前),匹配度最高。 5s 后抓栈很可能已离开真因。
工程意义:
- WatchDog 阈值设 2-3s(比系统 ANR 提前)。
- 触发后立即抓栈,不要等。
- 抓多次(每秒一次)建立时序,可还原"卡的过程"。
Step 8 — 边界
- 阈值太小(< 1s)会误报(普通卡顿就触发)。
- 抓栈本身也有开销(~1ms),频繁抓会影响性能。
# 17.3 实验三:预警提前度
Step 1 — 原始观察
某团队上线 ANR 监控后,发现 80% 的 ANR 之前都有"主线程消息 P99 > 1s"的事件。预警比 ANR 提前多久?
Step 2 — 提出疑问
ANR 触发前多长时间出现"主线程消息处理超时"前兆?
Step 3 — 形成假设
H₁:ANR 几乎都有可观测前兆,前兆出现到 ANR 触发通常 5-300 秒。
Step 4-5 — 设计 + 实测
某 App 真实数据 1000 次 ANR 事件:
| 前兆 → ANR 时间 | 占比 |
|---|---|
| 0-30s | 35% |
| 30-120s | 28% |
| 120-300s | 17% |
| > 300s | 5% |
| 无明显前兆 | 15% |
Step 6-7 — 结论
ANR 大多数有前兆。建立"前兆监控 + 主动归因"机制能在 ANR 发生前定位问题。
工程意义:
- 监控指标加入"主线程消息 P99"。
- 前兆出现时主动抓栈 + 上报,不等 ANR 触发。
- 长期看前兆趋势比看 ANR 数更敏感。
Step 8 — 边界
- 突发 ANR(外部干扰、系统抢占)无前兆,约占 15%。
- 不同业务前兆模式不同,需要按场景调阈值。
# 17.4 实验四:所有线程栈 vs 仅主栈的归因价值
Step 1 — 原始观察
很多 ANR 监控只抓主线程栈,分析时常常"主线程在 nativePollOnce / Object.wait,看不出问题"。只看主线程栈够吗?
Step 2 — 提出疑问
同一组 ANR 事件,分别只用"主线程栈"和"所有线程栈+Binder 远端栈"做归因,准确归到根因的比例分别是多少?
Step 3 — 形成假设
H₁:仅主栈在 off-CPU 类 ANR(占 ~70%)几乎无能为力;所有线程栈 + Binder 远端可把归因准确率从 < 30% 提升到 > 85%。
Step 4 — 设计实验
| 项 | 配置 |
|---|---|
| 数据集 | 某线上 1000 次 ANR 事件,已通过事后修复确认根因 |
| 方案 A | 仅看主线程栈 |
| 方案 B | 主线程栈 + 所有线程栈 + Binder 远端 |
| 主指标 | 归因到根因的准确率 |
Step 5 — 实测数据
| ANR 类型 | 仅主栈准确率 | 所有线程栈准确率 |
|---|---|---|
| on-CPU 类(业务计算) | 95% | 95% |
| 锁竞争类 | 8%(栈顶 wait/park) | 94%(看到持锁线程) |
| Binder 慢类 | 12%(栈顶 transact) | 88%(看到远端进程栈) |
| GC STW 类 | 30% | 92% |
| 系统抢占类 | 0% | 65%(需 sched 数据进一步) |
| 加权合计 | 27% | 86% |
Step 6-7 — 结论
ANR 现场必须抓"所有线程栈 + Binder 远端栈",不能只抓主线程。 仅看主线程栈对 70% 的 off-CPU 类 ANR 几乎无能为力。
工程意义:
- WatchDog 触发时遍历
Thread.getAllStackTraces()。 - Android Q+ 用
dumpJavaBacktraceToFileTimeout获取 Binder 远端栈。 - 上报字段加
lockHolderThread、binderRemotePid。
Step 8 — 边界
- 抓所有线程栈开销大(~10-50ms,视线程数),需限制频率。
- 系统抢占类仍需 Perfetto sched 数据补充。
# 17.5 实验五:SIGQUIT Hook 抓取 traces 文件
Step 1 — 原始观察
Android Q+ 普通应用读 /data/anr/traces.txt 受限。有没有办法在系统 dump 时同步获取?
Step 2 — 提出疑问
能否通过劫持 SIGQUIT 信号(系统通知应用 dump 栈用),主动抓取系统 dump 内容并上报?
Step 3 — 形成假设
H₁:系统在 ANR 前发 SIGQUIT 给目标进程让 ART dump 栈到文件,劫持这个信号能与系统同时 dump,得到与系统 traces.txt 一致的内容。
Step 4-5 — 设计 + 实测
| 设备 | dump 成功率 | 内容一致性 | 触发延迟 |
|---|---|---|---|
| Pixel 6 | 100% | 100% | < 50ms |
| 三星 S22 | 96% | 100%(成功时) | < 80ms |
| 小米 12(One UI 类似) | 88% | 100% | < 100ms |
Step 6-7 — 结论
SIGQUIT Hook 是 Android Q+ 抓 ANR 现场的最优解:无需 root,无需特殊权限,dump 内容与系统一致。
工程意义:
- KOOM / Matrix / xCrash 等开源库都基于此原理。
- 在 WatchDog 触发的同时调
kill(getpid(), SIGQUIT)主动 dump。 - 把 dump 内容加密上报,配合所有线程栈做完整归因。
Step 8 — 边界
- 部分 OEM ROM 默认拦截 SIGQUIT,需 fallback 到
Thread.getAllStackTraces()。 - iOS 没有等价机制,依赖 MetricKit。
# 17.6 五大实验启示
阈值精度 → 5s 是"事件等待",不是"主线程卡" ─┐
│
抓栈时机 → 2-3s 抓栈匹配度最高 │
│
预警提前度 → 85% ANR 有 30s+ 前兆 ├─▶ ANR 治理 = 早预警 + 全栈抓 + 早归因
│
所有线程栈 → off-CPU 类需全栈 + Binder 远端 │
│
SIGQUIT Hook → 与系统同源 dump,无需 root ─┘
2
3
4
5
6
7
8
9
统一启示:
- ANR 不是"突然出事":85% 都有可观测前兆。
- 早干预比晚兜底有效:WatchDog 2-3s 阈值 + 主线程 P99 监控。
- 栈不能只抓主线程:off-CPU 类(70%)必须看所有线程 + Binder 远端。
- 现场抓取要跟系统同源:SIGQUIT Hook 提供与系统 traces.txt 一致的内容。
- 数据驱动:阈值、抓栈时机、栈范围都有最优解,不是凭感觉。
# 18.实战案例
# 18.1 跨端同构案例:分级初始化降 ANR
背景:某社交应用 Android 上 ANR 率 0.3%,iOS 上启动 watchdog 0.08%,明显高于行业基准。
现象:
- Android:ANR trace 主要在
Application.onCreate中的 SDK 同步初始化。 - iOS:MetricKit 报 launch hang,主要在 dyld + 多个 +load 方法。
度量与归因:
两端共同特征:启动期同步初始化的第三方 SDK 太多。
假设与求证:
提出统一假设:"SDK 分级 init + 主线程减负"(同 卷四·01 §18.2 案例)。
修复:三端统一采用分级初始化框架。
验证:
| 平台 | 优化前 | 优化后 | 降幅 |
|---|---|---|---|
| Android ANR 率 | 0.30% | 0.09% | 70% |
| iOS launch watchdog | 0.08% | 0.02% | 75% |
统一启示:跨端 ANR 治理 = 启动减负 + 分级初始化 + 监控前兆。
# 18.2 平台特异案例:主线程 nativePollOnce 假象
背景:Android 上 ANR trace 显示主线程在 nativePollOnce(看似在等消息),但用户感知是"卡死了"。
现象:trace 看起来"正常"(主线程在 epoll 等消息),但实际用户卡 5+ 秒。
度量与归因:
进一步分析所有线程栈:
- 子线程 X 持有锁 L 后陷入网络等待。
- 主线程在 nativePollOnce 之前发送了一条消息,等 X 完成。
- X 的网络任务有 30s 超时,让主线程有效卡 30s。
假设与求证:
假设:主线程在等子线程的 IO,看似在 Looper 实际是被锁卡住。
实验:把网络超时从 30s 改为 5s + 主线程不等子线程结果。✅ ANR 消失。
修复:
- 严禁主线程同步等子线程。
- 锁内严禁 IO / 网络。
- 所有跨线程同步加超时。
验证:该模式 ANR 占比从 25% 降到 < 1%。
边界与上线策略:要全面 review 主线程消息处理逻辑,识别隐性等待。
# 18.3 大促 ANR 风暴案例
背景:§02 案例 详述。
关键教训:
- 栈顶 ≠ 真因:nativePollOnce / Object.wait 看起来正常,实际是被锁/等待卡住。
- 所有线程栈是关键:off-CPU 类 ANR 必须看其他线程在干什么。
- 大促场景特殊:连接池、Binder、DB 锁都可能成为瓶颈。
- 改大监控阈值是反模式:系统不会陪你自欺欺人。
# 18.4 案例统一启示
- 数据驱动而非经验:§02 案例 3 周失败 vs 6 天成功的根本差异。
- 栈不能只看主线程:off-CPU 类必须看全栈。
- WatchDog 是核心武器:2-3s 抓栈,与系统同源 dump。
- 跨线程协同是主战场:锁内禁 IO,跨线程加超时。
# 19.防劣化体系
# 19.1 三道防线总览
开发期 ──▶ 编译期 / CI ──▶ 上线期 / 运行期
│ │ │
▼ ▼ ▼
[Lint] [自动化基准] [线上 SLO]
2
3
4
# 19.2 编码期 Lint
| Lint 规则 | 作用 |
|---|---|
MainThreadIO | 主线程 IO / DB / 网络调用 → 错误 |
HeavyOnCreateInit | Application.onCreate 中调用 SDK init > 100ms → 警告 |
LongOnReceive | Broadcast onReceive 同步执行 > 1s 任务 → 警告 |
LockWithIO | 锁内调用 IO → 错误 |
MainThreadFutureGet | 主线程同步等待 Future / Promise → 警告 |
MissingGoAsync | onReceive 内有重任务但未 goAsync → 警告 |
# 19.3 CI 卡口与线上 SLO
CI 卡口:
- 启动期主线程消息 P99 < 100ms。
- 关键场景模拟用例不触发 WatchDog(2s 阈值)。
- 死锁专项检测。
线上 SLO:
| 指标 | 阈值 |
|---|---|
| ANR 率 | < 0.1% |
| 准 ANR(2s 阻塞)率 | < 0.5% |
| 主线程消息 P99 | < 500ms |
| iOS launch watchdog | < 0.05% |
| 错误预算耗尽 | 冻结主线程相关变更 |
# 19.4 监控数据闭环
线上 WatchDog + LooperPrinter + ApplicationExitInfo
↓
按"前兆类型 × 设备 × 网络 × 时段"细分
↓
异常上升告警
↓
定位到具体场景(关联线程栈 + Binder 远端)
↓
修复 + 回归 CI
↓
灰度验证 → 全量发布
2
3
4
5
6
7
8
9
10
11
# 20.跨平台速查
# 20.1 工具速查
| 平台 | 主线程监控 | WatchDog | 现场抓取 | 历史 ANR |
|---|---|---|---|---|
| Android | LooperPrinter | 自实现 HandlerThread | UncaughtException + traces.txt | ProcessExitReason (R+) |
| iOS | RunLoop Observer | GCD 子线程心跳 | NSSetUncaughtExceptionHandler + MetricKit | MetricKit |
| Web | longtask Performance | Worker 心跳 | window.onerror | N/A |
| 嵌入式 | 自定义 trace | RTOS watchdog | crash dump | 日志系统 |
# 20.2 关键 API 速查
| 操作 | Android | iOS | Web |
|---|---|---|---|
| 主线程消息 hook | Looper.setMessageLogging | CFRunLoopObserverCreate | PerformanceObserver({type:'longtask'}) |
| 异步 broadcast | goAsync() | N/A | event.waitUntil(promise) |
| 启动期任务调度 | App Startup | Operation chain | dynamic import() |
| 监控启动 hang | logcat Displayed + 自定义 | MetricKit MXLaunchHangDiagnostic | PerformanceObserver |
| 强制超时 | Future.get(timeout) | dispatch_after | Promise.race(timeout) |
# 20.3 通用 SLO 速查
| 指标 | 推荐值 |
|---|---|
| ANR 率(Android) | < 0.1% |
| iOS launch watchdog | < 0.05% |
| 准 ANR(2s 阻塞)率 | < 0.5% |
| 主线程消息 P99 | < 500ms |
| WatchDog 阈值 | 2-3s |
| 抓栈范围 | 所有线程 + Binder 远端 |
# 21.总结与延伸
# 21.1 五条核心原则
- ANR = 卡顿极端形态:治理 ANR 根本是治理卡顿;70% 是 off-CPU 类。
- 早预警比晚兜底:WatchDog 2-3s 比系统 5s 更早抓栈,匹配度从 27% 升到 86%(§17.2 + §17.4)。
- 现场必抓全栈:每次 ANR / 准 ANR 都要留下所有线程栈 + Binder 远端,仅主栈对 off-CPU 类无能为力。
- 前兆有 85%:主线程 P99 监控能提前 30s+ 预警(§17.3)。
- 顺应系统机制:goAsync / WorkManager / App Startup / SIGQUIT Hook 都是合规手段。
# 21.2 五个常见误区
- "5 秒卡就 ANR":错(必须有事件等待);改自己的监控阈值不能避免系统兜底。
- "主线程没卡就不会 ANR":错(off-CPU 等锁也会,§02 案例 70% 是这种)。
- "iOS 没 ANR 就没问题":错(启动 watchdog 同样致命,且无弹窗直接杀进程)。
- "ANR trace 一看就懂":trace 反映兜底时刻,且仅 Q+ 受限;必须配 SIGQUIT + 所有线程栈。
- "ANR 都是代码问题":错(系统抢占、温度降频也会;但应用必须做超时兜底,参考模式 B)。
# 21.3 三个外延
- AI ANR 预测:未来用 ML 分析前兆模式,提前预测 ANR 风险。
- 跨进程 ANR 协同:多进程应用的 ANR 关联归因。
- WebAssembly + Worker:Web 端通过 Worker 实现真正的"主线程卸载"。
# 21.4 给团队的建议
- 第 1 周:上线 WatchDog(2s 阈值)+ LooperPrinter,建立监控基线。
- 第 2 周:执行第一层治理(§13)——主线程 IO/DB 异步化。
- 第 3 周:执行第二层治理(§14)——锁内禁 IO + 跨线程加超时。
- 第 4 周:执行第三层治理(§15)——SIGQUIT Hook + 历史 ANR。
- 第 5 周以上:执行第四层治理(§16)+ 持续优化。
# 21.5 延伸阅读
- Android Vitals:ANRs
- WWDC 2019: Improving Battery Life and Performance
- WWDC 2020: Diagnose Performance Issues with the XCTest Framework
- Brendan Gregg:Systems Performance Chapter 6(CPU 调度延迟)
- Google IO: App Startup, ANRs, Crashes
- KOOM / Matrix / xCrash 开源库源码
# 一句话总结
ANR = 卡顿的极端形态,是"主线程被阻塞超过系统忍耐阈值"的兜底惩罚。 70% ANR 是 off-CPU 类(等锁/等 IO/等 Binder),主线程栈是无辜的,真因在其他线程或其他进程。 WatchDog 2-3s 阈值 + 所有线程栈 + SIGQUIT Hook 是治理铁三角。 §02 案例 那个"3 周经验派 vs 6 天方法派"的反差,正是这条路径的最锋利证据。