ANR 产生机制:Android 系统是如何盯上你的?

各位同学,今天我们聊聊 ANR 到底怎么被系统逮住的。说实话,我早年刚接触 Android 时,总以为 ANR 就是简单的「超时弹窗」。后来被线上 ANR 搞得焦头烂额,才明白背后的检测机制其实是一整套精密流程。

简单说,系统通过两种方式监控你的应用是否还活着:消息队列超时监控和WatchDog 兜底机制。前者管前台交互,后者管后台任务。咱们一个一个说。

1. 前台超时:Service、BroadcastReceiver、输入事件

系统会给每种组件定一个「死线」——超时阈值。一旦超过这个时间还没响应,系统就判定为 ANR。

不同组件的 ANR 超时阈值
组件类型超时阈值触发条件
Service20 秒(前台服务)
200 秒(后台服务)
onStartCommand / onBind 未在规定时间内返回
BroadcastReceiver10 秒onReceive 执行超时(包括异步的 goAsync)
输入事件(按键、触摸)5 秒主线程 Looper 没在 5 秒内处理完上一个事件
ContentProvider30 秒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 秒检查一次关键系统服务是否还活着。怎么检查?

  1. WatchDog 拿到 SystemServer 里所有重要服务的 monitor 锁。
  2. 它尝试对这些服务发一个「心跳」——说白了就是读取一个变量,或者调用一个简单方法。
  3. 如果某个服务在 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 检测的完整流程画成下面这个图。这样你可以直观看到每一步的时序和关联。

ANR 检测双轨道机制 轨道 A:前台组件超时检测 输入事件 5s Broadcast 10s Service 20s/200s ContentProvider 30s AMS 收到超时消息 → 生成 ANR 轨道 B:WatchDog 系统守护 每 30 秒检查关键服务 monitor 锁 60 秒无响应 → 判定服务死锁 直接重启 SystemServer 进程 最终结果:ANR 弹窗 / 进程重启 说明:两条轨道可能同时触发。例如后台任务导致主线程长时间阻塞,既可能触发输入事件超时(轨道 A),也可能让 AMS 失联从而被 WatchDog 杀掉(轨道 B)。

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 / 200s30s 周期 + 60s 无响应
触发后果弹出 ANR 对话框,可等待或关闭直接重启 SystemServer,全机重启
检测方式消息队列的延迟消息触发主动 monitor 锁检查
是否可被应用避免可以,只要主线程不阻塞较难,除非应用不卡死主线程且不阻塞 Binder

说白了,前台超时是「温柔版」检测——用户点一下「确定」还能继续用;WatchDog 是「硬核版」——一旦触发,整个系统都崩给你看。

嗯,这次的内容就到这里。理解了检测机制,下回遇到 ANR,你就知道应该先去查哪条路径了。