第二章:ANR排查全链路总览——现象→日志→堆栈→代码→修复→验证的闭环流程

ANR排查不是孤立的“抓一次堆栈就结束”的运气游戏,而是一条从用户感知到代码修复、再到线上验证的完整闭环。本章通过一张全链路流程图,将六个关键环节串联起来,让你在后续每一章中都能清晰知道自己处于哪个阶段。

核心认知: 排查闭环 = 现象识别 → 数据采集 → 原因定位 → 修复动作 → 复测与预防。缺失任何一环,问题都可能复现或漏过。

2.1 全链路闭环总览

下面的SVG流程图展示了从用户侧“App卡死”开始,经过五个阶段最终形成知识沉淀的完整路径。每个阶段都对应一组标准化动作。

ANR排查全链路闭环总览 ① 现象识别 “点击无响应” ② 数据采集 日志 / traces / 堆栈 ③ 原因定位 主线程阻塞点 ④ 修复动作 异步 / 缓存 / 降级 ⑤ 复测与验证 回归 / 线上监控 ⑥ 预防与沉淀 自动化规则 / 文档 持续迭代闭环 关键要点: • 现象识别必须包含“用户侧表现 + 系统侧证据”,避免主观误判。 • 数据采集环节要求“三件套”:logcat + traces.txt + 线程堆栈。 • 复测不能仅靠手动点,需引入ANR监听或监控SDK做线上验证。 • 预防是最容易被忽略的环节,至少应产出“ANR排查Checklist”。
闭环思维: 传统排查只做到“修复”就提交代码,但线上验证失败或遗漏的重现场景往往导致二次ANR。务必把“复测与预防”回路走完。

2.2 现象识别:从用户报障到系统标记

现象识别是起点,也是最容易犯错的一步。用户反馈的“手机很卡”不一定是ANR,也可能是系统抖动或应用死锁。精准区分需要结合两个证据:

  • 用户侧表现: 点击屏幕无反馈、App白屏、系统弹出“应用无响应”对话框。
  • 系统侧证据: logcat中存在 ANR in ... 关键字,或 /data/anr/traces.txt 文件生成。
// 典型ANR日志头(logcat过滤):
// 09-15 10:23:45.789  1234  5678 I ActivityManager: ANR in com.example.app
// PID: 4567
// Reason: Input dispatching timed out (Waiting to send non-key event because the touched window has not finished processing certain input events that were delivered to it over 500.0ms ago. Wait queue length: 5)
// Load: 18.65 / 17.22 / 15.98
// CPU usage from 5030ms to 0ms ago:
//   100% 4567/com.example.app: 97% user + 3% kernel / faults: 189 minor 1 major
//   0.3% 1234/system_server: 0.1% user + 0.2% kernel
常见误判: 不要仅凭“用户说卡”就开启堆栈抓取。应先确认logcat中是否有明确的ANR标记,否则可能把普通的界面卡顿(非ANR)当ANR分析,浪费大量时间。

2.3 数据采集:三件套缺一不可

一旦确认ANR发生,必须快速采集所有现场数据。标准三件套:

数据源路径/命令关键信息
logcat 日志adb logcat -b main -b system -b events > anr.logANR时间、原因、CPU/IO负载
traces.txt/data/anr/traces.txt(需root或bugreport)所有线程堆栈,主线程阻塞处
线程具体堆栈adb shell kill -3 <pid> 触发dump实时堆栈,用于对比
# 采集完整bugreport(包含traces和logcat):
adb bugreport bugreport_$(date +%Y%m%d_%H%M%S).zip

# 仅抓取traces:
adb shell su -c "cat /data/anr/traces.txt" > traces.txt

# 如果权限不够,可以用:
adb shell dumpsys activity processes > process_info.txt
时间戳对齐: 务必记录ANR日志中的精确时间(毫秒级别),与traces.txt中的时间戳匹配,避免分析到不同轮次的堆栈。

2.4 原因定位:主线程阻塞点分析

拿到堆栈后,核心任务是找出主线程(通常是 "main" 线程)被卡住的具体调用链。常见的阻塞模式有四种:

  • IO阻塞: 主线程读文件、网络请求、数据库查询。
  • 锁竞争: 等待其他线程持有的锁(synchronized / ReentrantLock)。
  • Binder等待: 调用系统服务(如ActivityManager)超时。
  • 计算密集: 大循环、复杂布局、JSON解析。
// 主线程堆栈示例(阻塞在SQLite查询):
"main" prio=5 tid=1 Blocked
  at android.database.sqlite.SQLiteDatabase.rawQueryWithFactory(Native Method)
  at android.database.sqlite.SQLiteDatabase.query(SQLiteDatabase.java:1024)
  at com.example.data.local.UserDao.getUser(UserDao.java:35)
  at com.example.viewmodel.UserViewModel.loadUser(UserViewModel.java:22)
  at com.example.ui.MainActivity.onCreate(MainActivity.java:48)
  - waiting on <0x1a2b3c4d> (a java.lang.Object)
  - locked <0x5e6f7080> (a java.lang.Object)
快速定位技巧: 在traces.txt中搜索 tid=1 或 "main",然后看堆栈最底部的 at ... () 或 waiting on / locked 关键字。如果是 waiting on,说明在等待锁;如果是 Native Method,可能是系统调用或IO。

2.5 修复动作:对症下药

根据定位到的阻塞类型,选择对应的修复策略:

阻塞类型典型修复动作注意事项
IO阻塞(读文件/网络)改用异步线程(AsyncTask/Coroutine/RxJava)或缓存数据确保线程池不被耗尽
锁竞争减小临界区、改用无锁结构、避免主线程获取锁分析锁持有者的超时时间
Binder超时缓存结果、降级方案、增加等待时长系统服务调用不可控,优先降级
计算密集分块处理、异步执行、使用性能优化算法注意布局渲染计算同样耗时间
// 修复示例:将数据库查询从主线程移到协程
// 之前(主线程阻塞)
// List users = db.userDao().getUsersSync();

// 之后(异步)
lifecycleScope.launch(Dispatchers.IO) {
    val users = withContext(Dispatchers.IO) {
        db.userDao().getUsersSync()
    }
    // 回到主线程更新UI
    withContext(Dispatchers.Main) {
        adapter.submitList(users)
    }
}

2.6 复测与预防:闭环收口

修复后必须经历三个验证层次:

  1. 单机复测: 手动复现原场景,确保不再出现ANR。建议使用 adb shell am start -W -n ... 测量启动耗时,配合 dumpsys 检查UI超时。
  2. 压力测试: 使用Monkey或UI自动化工具(如Espresso/Maestro)模拟高频操作。
  3. 线上验证: 部署代码后观察3~7天内的ANR率,使用第三方监控(如Firebase Performance / 自有APM)。
预防工作不可跳过: 即使修复了一个ANR,如果团队没有沉淀规则(例如“主线程不允许直接访问数据库”),同类问题会反复出现。建议在修复后补全以下产物:
  • 一份 ANR排查Checklist(包含采集命令、常见根因、修复模板)。
  • 一条 CI/代码审查规则:检测主线程是否调用了可能阻塞的API(如 Thread.sleep()、URLConnection 等)。
  • 一段 自动化测试用例:触发该场景并断言不出现ANR。
本章小结: 全链路闭环的核心是“先定性再定位、先采集再分析、先修复再验证、先解决再预防”。后续每一章都会深入到某个环节的实操细节,但始终以本章的总览图为路线图。