第2章:Android ANR机制详解
说起ANR,很多同学第一反应是「应用无响应弹窗」。但你真的理解它背后的触发逻辑吗?我个人习惯先把底层机制摸透,再去排查问题。这一章我们就来扒一扒ANR的“底裤”。
2.1 什么是ANR?
ANR全称Application Not Responding,系统认为你的应用“没救了”就会弹出这个对话框。说白了,就是Android系统给你设置了一个超时闹钟,到了时间你还没干完活,系统就判定你阻塞了主线程。
| 场景 | 超时阈值 | 常见原因 |
|---|---|---|
| 输入事件(按键、触摸) | 5秒 | 主线程耗时操作,如网络请求 |
| 广播接收(BroadcastReceiver) | 前台10秒 / 后台60秒 | onReceive里做大量计算或I/O |
| 服务(Service) | 前台20秒 / 后台200秒 | 服务里执行耗时长任务 |
| ContentProvider | 10秒 | 查询或写入超时 |
我曾在一个直播项目里遇到过,用户快速滑动列表时偶尔弹出ANR。排查后发现是某个列表项加载图片时做了同步解码,卡了主线程。嗯,那时候我才对「5秒」这个阈值有了血泪般的认识。
2.2 ANR的触发流程
系统是怎么知道你“无响应”的?我给大家拆解一下内部流程。其实核心就是三个角色的配合:监控者、调度者、执行者。
为什么会这样?我们看一下消息队列的套路:
- 发送消息:当要处理输入事件或广播时,系统往主线程Looper里发送一个普通消息,同时启动一个超时计时器。
- 超时计时:计时器的时间就是上面表格里的阈值。计时器到点后,会检查该消息是否已被处理。
- 触发判罚:如果消息还在队列里没被处理,说明主线程卡住了。系统就会向系统服务(例如ActivityManagerService)发送一个“ANR触发”的信号。
- 生成ANR:AMS收到信号后,会采集当前进程的线程堆栈、CPU使用率等信息,然后弹出ANR对话框,同时写入traces.txt。
// 伪代码示意:超时检测机制
void dispatchMessage(Message msg) {
startTimeout(msg, 5000); // 启动5秒超时
msg.target.handleMessage(msg);
cancelTimeout(msg);
}
你看,如果 handleMessage() 里执行了耗时任务,那 cancelTimeout() 就会延迟调用,超时机制自然就触发了。
2.3 主线程为什么会被卡住?
主线程卡住的根因是什么?本质上就是Looper循环卡住了,无法继续处理下一个消息。我总结了三个最常见的“凶手”:
- 同步I/O操作:比如在UI线程里读写文件、执行网络请求。哪怕是OkHttp的同步请求,也不能放在主线程。
- 长时间计算:比如Bitmap缩放、JSON解析过大列表。我记得有一次在播放器项目里,把视频缩略图生成放在了主线程,结果滑动seekBar就ANR了。
- 锁竞争导致死锁或活锁:主线程等一个锁,而持有锁的线程又在等主线程去处理某个回调。经典的死锁场景。
/data/anr/traces.txt,里面会明确记录主线程的堆栈和锁状态。如果你看到“waiting to lock”或“blocked on <0x..>”,基本就是锁的问题了。
2.4 ANR的判定维度
系统不是看到主线程卡了就立马弹框,它还要综合几个维度判断:
| 维度 | 说明 | 举例 |
|---|---|---|
| 消息处理超时 | 消息从入队到出队超过阈值 | 触摸事件5秒未响应 |
| CPU占用率 | 如果CPU空闲,系统可能放你一马 | 系统认为当前无负载压力 |
| 进程是否被杀 | 如果进程已经被kill,就不会弹ANR | 低内存场景 |
| 用户感知度 | 只有前台可见或可交互的进程才触发 | 后台进程超时但可能不弹框 |
你看,系统其实也不是很“严格”。但做播放器的时候,一旦出现ANR,用户就会直接卸载。所以,我们还是要彻底杜绝。
2.5 本章知识体系图
下面这张图总结了ANR的触发链条和关键节点。建议大家对照着记忆,排查问题的时候心里就有地图了。
2.6 避坑指南
我曾经在优化ExoPlayer播放器时,发现一个诡异的ANR:每次切码率时就会卡住。排查发现是ExoPlayer在主线程进行MediaCodec配置时调用了 MediaCodec.configure(),而这个方法在某些设备上可能耗时超过200毫秒。如果连续切换,积累下来就触发了5秒阈值。
还有一次,业务方在BroadcastReceiver的 onReceive() 里直接调了ExoPlayer的 prepare() 并等待完成。这一等就是十几秒,ANR稳稳地被触发。其实只要把 prepare() 扔到后台线程,问题就解决了。
android:process=":player" 可以隔离到独立进程,这样主进程的ANR就不会影响到播放流程。但要注意跨进程通信的开销。