第1章 ANR现象与分类:无响应类型、关键特征、堆栈触发时机

1.1 现象识别——什么是ANR?

ANR(Application Not Responding)是Android系统在检测到应用主线程无法在指定时间窗口内完成新事件处理时,主动弹出“应用无响应”对话框并记录堆栈的系统级保护机制。识别ANR的第一步是区分其触发场景与超时阈值。

关键超时阈值(Android 9+ 默认值):
输入事件分发:5s | 服务启动/绑定:20s | ContentProvider发布:10s | BroadcastReceiver onReceive:10s(前台)/ 60s(后台)
超出这些时间窗口而未完成,系统即触发ANR。
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(...)
  ...
ANR原因定位故障树(根据主线程状态与堆栈特征)
主线程状态 堆栈特征 根因大类 典型场景
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是否存在且主线程堆栈异常为准。
ANR分类与堆栈触发时序 输入事件超时(5s) 服务超时(20s) Provider/广播超时 系统判定ANR,生成traces.txt 主线程状态:RUNNABLE 堆栈特征:同一PC地址停留 >5s → CPU繁忙/计算阻塞 主线程状态:BLOCKED / WAIT 堆栈特征:“- waiting to lock <...>” → 锁竞争 / Binder挂起 修复:异步化 / 优化算法 避免主线程执行耗时计算 修复:减小锁范围 / 异步Binder 避免主线程等待锁或同步Binder

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)
  ...

问题定界步骤:

  1. 主线程状态=BLOCKED,等待锁 0x1234。
  2. 锁被 tid=15 (“sdb-writer”) 持有,而 tid=15 正执行 insert 处于 RUNNABLE(可能正在执行慢SQL)。
  3. 根因: 主线程UI点击触发query,但子线程写数据库长时间持有锁。修复方向:将query改为异步(AsyncQueryHandler或Coroutine with Dispatchers.IO),或缩短写锁时间。

复测: 模拟大量写入并发查询,监控主线程不再BLOCKED。