编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 性能优化实践

    • README
    • 公共方法论

    • 体系建设篇

    • 资源专项篇

    • 流水线专项

      • 渲染管线与原理
      • FPS与帧率检测
      • 卡顿捕获与归因
      • ANR监控与治理
        • 01.阅读说明
        • 02.贯穿案例
          • 2.1 案例背景
          • 2.2 经验派 3 周折腾(典型反面教材)
          • 2.3 方法派 6 天闭环
          • 2.4 上线效果
          • 2.5 案例如何串起本文
        • 03.ANR 本质定义
          • 3.1 ANR 的物理本质
          • 3.2 ANR 的现象与代价
          • 3.3 度量准则
          • 3.4 行业基准与目标
          • 3.5 8 个反直觉问题
        • 04.触发条件原理
          • 4.1 ANR 触发的四大类
          • 4.2 主线程"卡"的物理来源
          • 4.3 为什么 70% ANR 是 off-CPU 类
          • 4.4 跨平台同构原理
          • 4.5 平台差异点矩阵
        • 05.度量与采集
          • 5.1 三类捕获方案
          • 5.2 各方案的可见盲区
          • 5.3 跨平台采集对照表
          • 5.4 数据可信度评估
        • 06.归因决策树
          • 6.1 ANR 归因决策树
          • 6.2 现场快照分析
          • 6.3 系统级阻塞归因
          • 6.4 ANR 前兆识别
        • 07.输入事件全链路 ⭐
          • 7.1 输入事件的物理本质
          • 7.2 输入事件 ANR 的触发链路
          • 7.3 应用内的事件分发链
          • 7.4 输入事件 ANR 的应对
        • 08.Service 全链路 ⭐
          • 8.1 Service 的生命周期
          • 8.2 为何 foreground vs background 阈值不同
          • 8.3 Service 的现代替代
          • 8.4 Service ANR 的现场归因
        • 09.Broadcast 全链路 ⭐
          • 9.1 Broadcast 的分发机制
          • 9.2 onReceive 同步陷阱
          • 9.3 goAsync 的工作原理
          • 9.4 Broadcast 的现代替代
        • 10.ContentProvider 全链路 ⭐
          • 10.1 ContentProvider 的启动时机
          • 10.2 SDK 偷启动的陷阱
          • 10.3 App Startup 的解决方案
          • 10.4 CP ANR 的现场归因
        • 11.iOS Watchdog 全链路 ⭐
          • 11.1 iOS Watchdog 的物理本质
          • 11.2 iOS 启动 Watchdog 触发链路
          • 11.3 MetricKit 事后报告
          • 11.4 iOS 应用的 ANR 治理
        • 12.跨端 ANR 对照
          • 12.1 端到端流程对照表
          • 12.2 阈值对照
          • 12.3 统一启示
        • 13.治理一层减负
          • 13.1 一层命题
          • 13.2 策略 1.1:主线程 IO/DB/网络的强制异步化
          • 13.3 策略 1.2:onCreate / Application 启动期任务的分级调度
          • 13.4 策略 1.3:长任务切片(避免单次 > 50ms)
          • 13.5 一层反思
        • 14.治理二层协同
          • 14.1 二层命题
          • 14.2 策略 2.1:锁内严禁 IO/网络/Binder
          • 14.3 策略 2.2:跨线程同步加超时
          • 14.4 策略 2.3:连接池/线程池容量按峰值规划
          • 14.5 策略 2.4:Binder 调用加超时与降级
          • 14.6 二层反思
        • 15.治理三层监控
          • 15.1 三层命题
          • 15.2 策略 3.1:WatchDog 2-3s 阈值 + 全栈抓取
          • 15.3 策略 3.2:主线程消息 P99 持续监控
          • 15.4 策略 3.3:SIGQUIT Hook 与系统同源 dump
          • 15.5 策略 3.4:历史 ANR 补全统计
          • 15.6 三层反思
        • 16.治理四层组件
          • 16.1 四层命题
          • 16.2 策略 4.1:Broadcast 改 goAsync
          • 16.3 策略 4.2:Service / 长任务改 WorkManager
          • 16.4 策略 4.3:ContentProvider 启动期改 App Startup
          • 16.5 优先级判定(ROI)
          • 16.6 四层反思
        • 17.求证实验
          • 17.1 实验一:超时阈值精度
          • 17.2 实验二:现场抓取时机
          • 17.3 实验三:预警提前度
          • 17.4 实验四:所有线程栈 vs 仅主栈的归因价值
          • 17.5 实验五:SIGQUIT Hook 抓取 traces 文件
          • 17.6 五大实验启示
        • 18.实战案例
          • 18.1 跨端同构案例:分级初始化降 ANR
          • 18.2 平台特异案例:主线程 nativePollOnce 假象
          • 18.3 大促 ANR 风暴案例
          • 18.4 案例统一启示
        • 19.防劣化体系
          • 19.1 三道防线总览
          • 19.2 编码期 Lint
          • 19.3 CI 卡口与线上 SLO
          • 19.4 监控数据闭环
        • 20.跨平台速查
          • 20.1 工具速查
          • 20.2 关键 API 速查
          • 20.3 通用 SLO 速查
        • 21.总结与延伸
          • 21.1 五条核心原则
          • 21.2 五个常见误区
          • 21.3 三个外延
          • 21.4 给团队的建议
          • 21.5 延伸阅读
        • 一句话总结
      • 页面UI与布局优化
      • 动画交互响应优化
    • 业务专项篇

    • 交付防御篇

  • 程序编程原理

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

  • 专栏
  • 性能优化实践
  • 流水线专项
杨充
2026-05-27
目录

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 ← 兜底
1
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 个反直觉问题

带着这些问题阅读:

  1. ANR 5 秒阈值是固定的吗?
  2. 子线程死锁会触发 ANR 吗?
  3. 主线程没卡顿,怎么也会 ANR?
  4. iOS 没 ANR,是不是没 Watchdog 问题?
  5. ANR 弹出时主线程在做什么?能从堆栈准确归因吗?
  6. ANR Trace 文件保存在哪?怎么读?
  7. 后台进程也会 ANR 吗?
  8. 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 太重                          │
   └────────────────────────────────────────────────┘
1
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            │
   ├──────────────────────────────────────────────┤
   │ ③ 系统级阻塞:主线程被压制                      │
   │    高负载、降频、其他进程抢占                   │
   └──────────────────────────────────────────────┘
1
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 秒内响应了吗?]
                            │
                       ┌────┴────┐
                       ▼         ▼
                      是 → 正常    否 → 触发兜底
                                 │
                                 ▼
                        弹框 / 杀进程 / 重启
1
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 文件)
1
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 改造
1
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

关键事实:

  1. 事件分发是 IPC 异步——system_server 发,应用收,应用要 ack。
  2. 5s 超时计时点是"system_server 发出事件" —— 不是"应用收到事件"。
  3. 应用必须在 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 / 静默记录
1
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
1
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
1
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× 宽松)
1
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
       ├─ 锁等待 → 锁治理
       └─ 业务计算 → 切片 / 算法优化
1
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(同步串行)
       │
       ▼
   全部完成 → 广播结束
1
2
3
4
5
6
7
8
9
10
11
12
13

关键事实:

  1. Broadcast 默认串行——一个 Receiver 慢会拖累所有后续。
  2. onReceive 默认在主线程——同步阻塞即 ANR。
  3. 超时阈值:前台进程 60s,后台进程 10s。

# 9.2 onReceive 同步陷阱

// ❌ 反例:主线程同步做 IO
@Override
public void onReceive(Context context, Intent intent) {
    String data = readFromDisk();  // 主线程 IO
    db.insert(data);                // 主线程 DB
    // 超过 10s = ANR
}
1
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(); }
    });
}
1
2
3
4
5
6
7
8
9

# 9.3 goAsync 的工作原理

   onReceive 调用 goAsync()
       │
       ▼
   返回 PendingResult,告诉 system_server "我还在处理"
       │
       ▼
   onReceive 直接返回(避免主线程阻塞)
       │
       ▼
   后台线程异步处理
       │
       ▼
   完成 → result.finish() 通知系统
1
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
1
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 创建 / 显示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

关键事实:

  1. CP onCreate 在 Application.onCreate 之前——很多 SDK 利用这点"偷启动"。
  2. CP onCreate 串行执行——一个慢拖所有。
  3. 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 各自注册 -->
1
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 集成?
1
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                              │
   └──────────────────────────────────────────┘
1
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 内异步报告
1
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 注册
1
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());
1
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) }
}
1
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)
}
1
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);  // 下一帧继续
    }
}
1
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;
}
1
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;  // 兜底降级
}
1
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();
1
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);  // 不阻塞调用方
}
1
2
3
4

收益:模式 B(28%)大幅降低;oneway 调用不阻塞主线程。

边界:oneway 不能有返回值;需要返回值时用 RemoteCallback 异步回调。

# 14.6 二层反思

探索性思考:为什么"跨线程协同"是 ANR 治理的最大盲区? 因为它违反工程师的直觉——直觉认为"加锁是为了保护数据",但锁本身可能阻塞主线程。多数 ANR bug 的根因:

  1. 一开始 lock { db.write() } 是合理的(DB 是锁内)。
  2. 后来加了 lock { db.write(); upload() }("反正都在锁内")。
  3. upload() 是网络调用,可能 30s。
  4. 主线程下次 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);
            }
        }
    }
}
1
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

机理:§17.5 SIGQUIT Hook 实验。

做法:核心是劫持 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() 等价物
1
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) }
1
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

机理:§09.3 goAsync 工作原理。

做法:

@Override
public void onReceive(Context context, Intent intent) {
    PendingResult result = goAsync();
    executor.submit(() -> {
        try { handleHeavyTask(intent); }
        finally { result.finish(); }
    });
}
1
2
3
4
5
6
7
8

收益:C 类 ANR 几乎消除。

边界:goAsync 仍有 10s(前台)/ 60s(后台)上限;超过应改 WorkManager。

# 16.3 策略 4.2:Service / 长任务改 WorkManager

机理:§08.3 Service 现代替代。

做法:

val work = OneTimeWorkRequestBuilder<UploadWorker>()
    .setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
    .build()
WorkManager.getInstance(context).enqueue(work)
1
2
3
4

收益:B 类 ANR 几乎消除;同时合规且可靠(WorkManager 处理重试)。

边界:WorkManager 调度延迟约 10s-15min(视约束),实时性差任务不适用。

# 16.4 策略 4.3:ContentProvider 启动期改 App Startup

机理:§10.3 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>
1
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            ─┘
1
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 案例 详述。

关键教训:

  1. 栈顶 ≠ 真因:nativePollOnce / Object.wait 看起来正常,实际是被锁/等待卡住。
  2. 所有线程栈是关键:off-CPU 类 ANR 必须看其他线程在干什么。
  3. 大促场景特殊:连接池、Binder、DB 锁都可能成为瓶颈。
  4. 改大监控阈值是反模式:系统不会陪你自欺欺人。

# 18.4 案例统一启示

  • 数据驱动而非经验:§02 案例 3 周失败 vs 6 天成功的根本差异。
  • 栈不能只看主线程:off-CPU 类必须看全栈。
  • WatchDog 是核心武器:2-3s 抓栈,与系统同源 dump。
  • 跨线程协同是主战场:锁内禁 IO,跨线程加超时。

# 19.防劣化体系

# 19.1 三道防线总览

开发期 ──▶ 编译期 / CI ──▶ 上线期 / 运行期
   │             │              │
   ▼             ▼              ▼
[Lint]      [自动化基准]     [线上 SLO]
1
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
          ↓
   灰度验证 → 全量发布
1
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 五条核心原则

  1. ANR = 卡顿极端形态:治理 ANR 根本是治理卡顿;70% 是 off-CPU 类。
  2. 早预警比晚兜底:WatchDog 2-3s 比系统 5s 更早抓栈,匹配度从 27% 升到 86%(§17.2 + §17.4)。
  3. 现场必抓全栈:每次 ANR / 准 ANR 都要留下所有线程栈 + Binder 远端,仅主栈对 off-CPU 类无能为力。
  4. 前兆有 85%:主线程 P99 监控能提前 30s+ 预警(§17.3)。
  5. 顺应系统机制:goAsync / WorkManager / App Startup / SIGQUIT Hook 都是合规手段。

# 21.2 五个常见误区

  1. "5 秒卡就 ANR":错(必须有事件等待);改自己的监控阈值不能避免系统兜底。
  2. "主线程没卡就不会 ANR":错(off-CPU 等锁也会,§02 案例 70% 是这种)。
  3. "iOS 没 ANR 就没问题":错(启动 watchdog 同样致命,且无弹窗直接杀进程)。
  4. "ANR trace 一看就懂":trace 反映兜底时刻,且仅 Q+ 受限;必须配 SIGQUIT + 所有线程栈。
  5. "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 天方法派"的反差,正是这条路径的最锋利证据。

上次更新: 2026/06/07, 10:26:12
卡顿捕获与归因
页面UI与布局优化

← 卡顿捕获与归因 页面UI与布局优化→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式