10、ANR 机制:AMS检测ANR的原理、超时时间、trace文件分析
ANR,全称 Application Not Responding。说白了,就是你的应用卡住了,系统看不下去了,直接弹个对话框问用户「是要继续等,还是关掉它?」。
我在刚做 Android 开发那会儿,第一次遇到 ANR 还挺慌的。后来深入 Framework 层才发现,其实 ANR 的检测逻辑并不复杂,核心就在 AMS 里。今天我就带你把它彻底拆开看看。
10.1 ANR 的触发场景
系统不会无缘无故说你 ANR。只有以下四种情况,AMS 才会出手:
- Service 超时:前台 Service 20 秒没执行完,后台 Service 200 秒没执行完。
- BroadcastReceiver 超时:前台广播 10 秒没处理完,后台广播 60 秒没处理完。
- ContentProvider 超时:ContentProvider 发布时,如果 10 秒内没完成创建。
- Input 事件超时:按键或触摸事件 5 秒内没被处理。
嗯,这里要注意:Input 超时是最常见的。我遇到过很多次,用户点了一下屏幕,主线程正在做耗时操作,5 秒没响应,ANR 就来了。
10.2 AMS 检测 ANR 的原理
AMS 怎么知道你卡住了?它其实是个「定时炸弹」机制。
每次你启动一个组件(比如 Service、BroadcastReceiver),AMS 就会往内部发一个延时消息。这个延时消息就是超时时间。如果你在规定时间内完成了任务,就主动把这个消息移除掉。如果没移除,消息到了,AMS 就判定你 ANR 了。
我画了一张流程图,帮你理解这个逻辑:
你看,核心就是 延时消息 + 取消机制。AMS 内部用的是 Handler 的 sendMessageDelayed 来实现的。具体代码在 ActivityManagerService.java 里,我带你看看关键部分:
// 以 Service 超时为例
void serviceTimeout(ProcessRecord proc) {
if (proc != null) {
// 发送延时消息
Message msg = mHandler.obtainMessage(SERVICE_TIMEOUT_MSG, proc);
mHandler.sendMessageDelayed(msg, proc.execServicesFg ?
SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
}
}
// 当 Service 执行完成后,移除消息
void serviceDone(ProcessRecord proc) {
mHandler.removeMessages(SERVICE_TIMEOUT_MSG, proc);
}
说白了,就是「你按时交作业,我就不点名;你没交,我就点名」。这个设计很巧妙,也简单高效。
10.3 超时时间一览
不同的组件,超时时间不一样。我整理了一张表,方便你查阅:
| 组件类型 | 前台超时 | 后台超时 | 备注 |
|---|---|---|---|
| Service | 20 秒 | 200 秒 | 前台指有可见 Activity 绑定 |
| BroadcastReceiver | 10 秒 | 60 秒 | onReceive 中不能做耗时操作 |
| ContentProvider | 10 秒 | 10 秒 | 仅限发布阶段 |
| Input 事件 | 5 秒 | 5 秒 | 不分前后台 |
重点提醒:Input 超时 5 秒是最容易被用户感知的。你想想看,用户点了一下屏幕,5 秒没反应,他肯定觉得手机卡死了。所以大部分 ANR 都是 Input 超时。
10.4 Trace 文件分析
当 ANR 发生时,系统会生成一个 trace 文件。这个文件记录了所有线程的堆栈信息。默认路径是 /data/anr/traces.txt。
我建议你拿到 trace 文件后,按以下步骤分析:
- 先看主线程:找到 "main" 线程的堆栈,看它卡在哪个方法上。
- 看锁等待:如果主线程在等待某个锁,找到那个锁被谁持有。
- 看 CPU 使用率:trace 文件开头会显示 CPU 负载,如果 CPU 很高,可能是计算密集型任务;如果 CPU 很低,可能是死锁或 I/O 等待。
- 看 Binder 调用:如果主线程在 Binder 通信,检查对端进程是否正常。
我曾经遇到过一个案例:主线程卡在 SharedPreferences 的 apply() 方法上。你猜怎么着?是因为另一个线程在频繁写 SP,导致锁竞争。主线程等不到锁,就 ANR 了。
小技巧:用 adb pull /data/anr/traces.txt 把 trace 文件拉到本地。然后用文本编辑器搜索 "main" 关键字,快速定位主线程堆栈。
10.5 如何避免 ANR
避免 ANR 的核心原则就一条:别在主线程做耗时操作。但具体怎么做,我总结了几点:
- 网络请求、数据库读写、文件 I/O:统统放到子线程。
- BroadcastReceiver 的 onReceive:不要在里面做超过 10 秒的事。如果确实需要,启动一个 Service 去做。
- Service 的 onCreate/onStartCommand:同样不要阻塞。可以用 IntentService 或 WorkManager。
- ContentProvider 的 onCreate:这个是在主线程调用的,初始化要快。
注意:即使用了子线程,如果子线程持有主线程需要的锁,也会导致 ANR。我曾经排查过一个死锁导致的 ANR,两个线程互相等待对方的锁,主线程也被卷进去了。所以,锁的使用一定要小心。
10.6 总结
ANR 机制说白了就是 AMS 用延时消息做的一个「超时检测器」。你只要记住:主线程别卡,卡了就 ANR。trace 文件是排查 ANR 的第一手资料,学会看它,你就能快速定位问题。
我个人习惯是,每次发布版本前,都会用 Monkey 跑一遍,看看有没有 ANR。虽然不能完全覆盖,但至少能发现一些明显的坑。
好了,这一章就到这里。记住,ANR 不可怕,可怕的是你不知道它为什么发生。