第二章:ANR排查全链路总览——现象→日志→堆栈→代码→修复→验证的闭环流程
ANR排查不是孤立的“抓一次堆栈就结束”的运气游戏,而是一条从用户感知到代码修复、再到线上验证的完整闭环。本章通过一张全链路流程图,将六个关键环节串联起来,让你在后续每一章中都能清晰知道自己处于哪个阶段。
核心认知: 排查闭环 = 现象识别 → 数据采集 → 原因定位 → 修复动作 → 复测与预防。缺失任何一环,问题都可能复现或漏过。
2.1 全链路闭环总览
下面的SVG流程图展示了从用户侧“App卡死”开始,经过五个阶段最终形成知识沉淀的完整路径。每个阶段都对应一组标准化动作。
闭环思维: 传统排查只做到“修复”就提交代码,但线上验证失败或遗漏的重现场景往往导致二次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.log | ANR时间、原因、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 复测与预防:闭环收口
修复后必须经历三个验证层次:
- 单机复测: 手动复现原场景,确保不再出现ANR。建议使用
adb shell am start -W -n ...测量启动耗时,配合dumpsys检查UI超时。 - 压力测试: 使用Monkey或UI自动化工具(如Espresso/Maestro)模拟高频操作。
- 线上验证: 部署代码后观察3~7天内的ANR率,使用第三方监控(如Firebase Performance / 自有APM)。
预防工作不可跳过: 即使修复了一个ANR,如果团队没有沉淀规则(例如“主线程不允许直接访问数据库”),同类问题会反复出现。建议在修复后补全以下产物:
- 一份 ANR排查Checklist(包含采集命令、常见根因、修复模板)。
- 一条 CI/代码审查规则:检测主线程是否调用了可能阻塞的API(如
Thread.sleep()、URLConnection等)。 - 一段 自动化测试用例:触发该场景并断言不出现ANR。
本章小结: 全链路闭环的核心是“先定性再定位、先采集再分析、先修复再验证、先解决再预防”。后续每一章都会深入到某个环节的实操细节,但始终以本章的总览图为路线图。