第1章 ANR现象与分类:无响应类型、关键特征、堆栈触发时机
1.1 现象识别——什么是ANR?
ANR(Application Not Responding)是Android系统在检测到应用主线程无法在指定时间窗口内完成新事件处理时,主动弹出“应用无响应”对话框并记录堆栈的系统级保护机制。识别ANR的第一步是区分其触发场景与超时阈值。
关键超时阈值(Android 9+ 默认值):
输入事件分发:5s | 服务启动/绑定:20s | ContentProvider发布:10s | BroadcastReceiver onReceive:10s(前台)/ 60s(后台)
超出这些时间窗口而未完成,系统即触发ANR。
输入事件分发:5s | 服务启动/绑定:20s | ContentProvider发布:10s | BroadcastReceiver onReceive:10s(前台)/ 60s(后台)
超出这些时间窗口而未完成,系统即触发ANR。
| 类型 | 触发场景 | 关键特征 |
|---|---|---|
| InputDispatch | 主线程未处理按键/触摸事件(5s) | Logcat包含InputEventReceiver::dispatchInputEvent或channel '...' ~ Consumer closed input channel |
| ServiceTimeout | Service的onStartCommand或onBind超时(20s) |
堆栈显示android.app.ActivityThread$H 或 ActivityManagerService 相关消息 |
| ContentProviderTimeout | ContentProvider发布时onCreate超时(10s) |
主线程卡在installProvider 或 ContentProvider初始化逻辑 |
| BroadcastReceiverTimeout | 广播接收器 onReceive 超时 | Logcat出现Broadcast of intent { act=... } 并关联主线程长时间不返回 |
1.2 数据采集——堆栈从哪里来?
系统在判定ANR后,会立即将当时的进程所有线程堆栈写入/data/anr/traces.txt(Android 11+改为traces_${package}.txt),同时通过logcat输出ANR in ...关键行。堆栈的触发时机是时间窗口到期的瞬间,因此采集的核心是:
- 实时 logcat:出现“ANR in”后立即拉取(
adb bugreport或adb pull /data/anr/) - 事后痕迹:若用户已点击“等待”,系统不会再次收集;应引导用户复现并抓取完整bugreport
- 关键判断规则:若traces.txt中主线程(
main)状态为RUNNABLE+ 长时间停留在同一虚拟地址(PC),极大概率是CPU耗尽;若状态为BLOCKED或WAIT,则锁竞争或Binder挂起。
崩溃排查第一原则: 永远先看主线程("main")的堆栈入口,辅以CPU占比(
CPU usage from ... 日志段)和锁等待的- waiting on <...> 标记。
1.3 原因定位——故障树与排查表
根据主线程状态与堆栈特征,可以快速定位根因大类:
# 从traces.txt提取主线程状态示例
"main" prio=5 tid=1 Native
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x...
| self=0x...
| sysTid=12345 nice=-10 cgrp=default sched=... cpuset=...
| schedstat=( 2000000 400000 100 )
# 状态字段:RUNNABLE / BLOCKED / WAIT / TIMED_WAIT / SLEEPING
at com.example.MyClass.heavyLoop(MyClass.java:50)
- waiting to lock <0x...> (a java.lang.Object) // 表示正在等待锁
at com.example.MyClass.onClick(...)
...
| 主线程状态 | 堆栈特征 | 根因大类 | 典型场景 |
|---|---|---|---|
| RUNNABLE | 长时间停留在同一行代码(CPU密集) | ① 主线程CPU繁忙 | 大文件解析、Bitmap解码、循环计算 |
| BLOCKED | 出现 - waiting to lock <...> 且锁正被其他线程持有 |
② 锁竞争 | 主线程持锁被子线程长时间占用(如数据库写锁) |
| WAIT / TIMED_WAIT | 出现在 Object.wait() 或 Thread.sleep() 或在Binder同步调用中 |
③ Binder挂起 | 主线程发起同步Binder调用(如ContentProvider query)超时 |
| WAIT | 出现在 ConditionVariable.block() 或 CountDownLatch.await() |
④ 死锁/信号量等待 | 多个线程互相等待对方释放锁 |
1.4 修复动作与复测
根据上述故障树,修复方向如下:
- CPU繁忙:将耗时任务移至子线程(
AsyncTask/HandlerThread/Kotlin协程),使用性能剖析工具优化算法。 - 锁竞争:减少临界区范围,避免主线程持有锁等待子线程;考虑
ReadWriteLock或无锁数据结构。 - Binder挂起:将同步Binder调用改为异步回调,或设置超时机制;检查服务端响应速度。
- 死锁:统一锁获取顺序,使用
Lock接口支持超时,通过jstack或dump验证移除循环等待。
复测要点: 在真机低端设备、开启不保留活动(Don’t keep activities)和后台进程限制(Background process limit)条件下,重复触发相同场景,用adb logcat -b events | grep anr 确认无ANR产生。
1.5 预防——ANR防御体系
- 监控:线上使用
BlockCanary或Matrix卡顿监控,及时发现超过2s的卡顿。 - 架构:强制所有I/O、网络、复杂计算运行在子线程,主线程只做UI刷新。
- 测试:在CI流水线添加ANR检测步骤,自动解析traces.txt并判断主线程状态。
避坑提示: 不要仅依靠logcat搜索“ANR”来判断是否发生ANR——系统可能因弹窗被用户点击“等待”而并未弹出对话框,但traces.txt依然会生成。务必以traces.txt是否存在且主线程堆栈异常为准。
1.6 案例实战:快速定界
假设收到一份traces.txt,主线程堆栈如下:
"main" prio=5 tid=1 BLOCKED
at com.example.db.DatabaseHelper.query(DatabaseHelper.java:45)
- waiting to lock <0x1234> (a android.database.sqlite.SQLiteDatabase)
at com.example.ui.MainActivity.onClick(MainActivity.java:120)
...
"sdb-writer" prio=5 tid=15 RUNNABLE
at com.example.db.DatabaseHelper.insert(DatabaseHelper.java:80)
- locked <0x1234> (a android.database.sqlite.SQLiteDatabase)
...
问题定界步骤:
- 主线程状态=BLOCKED,等待锁
0x1234。 - 锁被 tid=15 (“sdb-writer”) 持有,而 tid=15 正执行
insert处于 RUNNABLE(可能正在执行慢SQL)。 - 根因: 主线程UI点击触发query,但子线程写数据库长时间持有锁。修复方向:将query改为异步(
AsyncQueryHandler或Coroutine with Dispatchers.IO),或缩短写锁时间。
复测: 模拟大量写入并发查询,监控主线程不再BLOCKED。