ANR 产生机制:Android 系统是如何盯上你的?
各位同学,今天我们聊聊 ANR 到底怎么被系统逮住的。说实话,我早年刚接触 Android 时,总以为 ANR 就是简单的「超时弹窗」。后来被线上 ANR 搞得焦头烂额,才明白背后的检测机制其实是一整套精密流程。
简单说,系统通过两种方式监控你的应用是否还活着:消息队列超时监控和WatchDog 兜底机制。前者管前台交互,后者管后台任务。咱们一个一个说。
1. 前台超时:Service、BroadcastReceiver、输入事件
系统会给每种组件定一个「死线」——超时阈值。一旦超过这个时间还没响应,系统就判定为 ANR。
| 组件类型 | 超时阈值 | 触发条件 |
|---|---|---|
| Service | 20 秒(前台服务) 200 秒(后台服务) | onStartCommand / onBind 未在规定时间内返回 |
| BroadcastReceiver | 10 秒 | onReceive 执行超时(包括异步的 goAsync) |
| 输入事件(按键、触摸) | 5 秒 | 主线程 Looper 没在 5 秒内处理完上一个事件 |
| ContentProvider | 30 秒 | publish 超时或查询耗时过长 |
为什么会设不同的阈值?你想想看,输入事件关乎用户体验,用户等 5 秒就已经要骂娘了。而后台 Service 如果正在下载大文件,给 200 秒上限其实已经算很宽松了。
我记得有个项目,线上爆了大量「输入事件 ANR」。查下来发现是 SharedPreference.commit() 在主线程写了一个巨大的 JSON,每次耗时 2~4 秒。5 秒阈值看着宽裕,但用户操作一旦连续触发,很容易踩线。
核心机制:系统在派发消息前会注册一个延迟消息(超时消息)。如果主线程在规定时间内没有消费掉前置消息并清空队列,延迟消息就会被回调,触发 ANR 弹窗。
2. 后台任务超时与 ANR 的关联:WatchDog 机制
上面说的前台超时,系统是靠 ActivityManagerService(AMS)里的 BroadcastQueue / ServiceRecord 自身 timer 来监控的。但还有一种特殊情况——后台任务超时导致主线程卡死,这时靠的是另一个守护者:WatchDog。
WatchDog 是 Android 系统层的一个独立线程(android.server.WatchDog),它每隔 30 秒检查一次关键系统服务是否还活着。怎么检查?
- WatchDog 拿到 SystemServer 里所有重要服务的 monitor 锁。
- 它尝试对这些服务发一个「心跳」——说白了就是读取一个变量,或者调用一个简单方法。
- 如果某个服务在 60 秒内(两个 check 周期)都没有响应,WatchDog 就会直接把这个服务对应的进程杀掉。
这跟咱们 ANR 有什么关系?嗯,这里要注意:如果你的后台任务(比如一个 IntentService 里的死循环)把主线程卡死了,AMS 自己可能都没法正常调度超时消息——因为连 AMS 自己都在等主线程 Looper。这时 WatchDog 就上场了。它会发现 AMS 的 monitor 锁拿不到,于是认定 AMS 死了,进而……你猜怎么着?直接 重启 SystemServer。
我曾经踩过一个坑:在后台 Service 里用了 new Thread().join(),但那个线程卡在了一个第三方 SDK 的同步锁上。主线程被 join 阻塞,导致前台触摸事件超时。AMS 发现了输入事件超时,弹了 ANR 弹窗。但更严重的是,这个 ANR 触发了 WatchDog 的连续检测失败,最后把 SystemServer 整重启了——全机重启!
3. 超时检测的完整链路
我习惯把 ANR 检测的完整流程画成下面这个图。这样你可以直观看到每一步的时序和关联。
4. 触发条件的归纳总结
好,咱们把触发条件理一理。一句话总结:主线程忙不过来且超过阈值,就会被系统判死刑。但具体场景有几种典型情况:
- 主线程阻塞:耗时的 I/O、网络请求、
Bitmap解码、View测量布局等直接在主线程执行。 - 锁竞争死锁:两个线程互相等对方释放锁,其中一个是主线程。我遇到过因为 Binder 线程池满导致主线程等待 Binder 响应,而 Binder 线程又在等主线程处理消息——完美死锁。
- CPU 饥饿:后台有大量计算密集型线程(比如一个
while(true)死循环),把 CPU 吃满,主线程分不到时间片。 - 内存不足导致 GC 风暴:内存频繁分配触发大量 GC,GC 时会暂停所有线程(包括主线程),如果暂停总时长超过 5 秒,就 ANR 了。
个人小技巧:平常看 ANR trace 文件时,重点关注 main 线程里 at 开头的堆栈。如果发现主线程卡在 Looper.loop() 里,说明是消息队列没有新消息进来?不是的——那是正常状态。实际上你要找的是 at ... 下面紧跟着的 waiting on 或 locked 字样,那才是真凶。
5. 两种检测机制的对比
| 维度 | 前台组件超时(AMS) | WatchDog(SystemServer) |
|---|---|---|
| 检测目标 | 单个应用的主线程响应 | SystemServer 内部关键服务存活 |
| 超时粒度 | 5s / 10s / 20s / 200s | 30s 周期 + 60s 无响应 |
| 触发后果 | 弹出 ANR 对话框,可等待或关闭 | 直接重启 SystemServer,全机重启 |
| 检测方式 | 消息队列的延迟消息触发 | 主动 monitor 锁检查 |
| 是否可被应用避免 | 可以,只要主线程不阻塞 | 较难,除非应用不卡死主线程且不阻塞 Binder |
说白了,前台超时是「温柔版」检测——用户点一下「确定」还能继续用;WatchDog 是「硬核版」——一旦触发,整个系统都崩给你看。
嗯,这次的内容就到这里。理解了检测机制,下回遇到 ANR,你就知道应该先去查哪条路径了。