ANR原理与类型:输入事件超时、服务超时、广播超时等触发机制
ANR,全称 Application Not Responding,就是应用无响应。说白了,当主线程被长时间阻塞,系统就会弹出那个烦人的对话框:「××应用无响应,是否继续等待?」
我接手过不少ANR案例,大部分都跟对象泄漏直接相关。但想根治,得先搞清楚系统到底是怎么判定ANR的。今天我们就聊聊三种经典触发机制:输入事件超时、服务超时、广播超时。
核心触发机制总览
系统通过几个核心组件来监控主线程的响应能力。一旦超时阈值被突破,ANR就发生了。下面这张图可以帮你快速建立认知框架:
输入事件超时 —— 最常见的ANR
Android系统规定,如果应用在 5秒 内没有对输入事件(触摸、按键)作出响应,系统就会判定ANR。这个5秒从事件被派发到主线程开始算,直到处理完并反馈完成。
我在项目中遇到过几次,明明代码里没有死循环,却频繁弹出ANR。后来一查,是主线程在等待某个锁,而那个锁被一个泄漏的Activity持有。
解决办法很简单:加一个阀门,控制并发数量。
服务超时 —— 容易被忽视的“20秒”
Service的ANR阈值是 20秒,分为两种情况:
- 前台服务:如果Service的
onStartCommand()或onBind()执行超过20秒,系统会报ANR。 - 后台服务:同样是20秒,但从Android 12开始,后台服务的启动有更严格的限制,超时后直接杀掉进程。
我个人的习惯是,所有Service里的耗时操作一律扔到子线程,绝不直接在主线程里做。你想想看,20秒看起来很宽容,但如果你在主线程里做了一次全量数据库查询,加上对象泄漏导致的内存碎片,GC时间一长,20秒眨眼就过去了。
| 组件 | 超时时间 | 典型触发场景 |
|---|---|---|
| 输入事件 | 5秒 | Touch事件未及时消费、主线程死锁 |
| 前台服务 | 20秒 | onStartCommand内执行耗时任务 |
| 后台服务 | 20秒 | 后台启动服务时主线程阻塞 |
| 广播接收器 | 10秒 | onReceive内同步执行网络/数据库操作 |
广播超时 —— 10秒的“隐形杀手”
广播接收器的超时是 10秒。为什么说它隐形?因为很多开发者以为广播接收器是在单独线程执行,其实它默认是在 主线程 上跑的。
我记得有一次线上问题:某个广播接收器处理了短信验证码自动填充,结果因为对象泄漏导致SharedPreferences写入时持有锁,onReceive()卡了12秒……ANR就出现了。
goAsync(),然后在子线程中完成任务。但要注意:goAsync()也有一个内部超时,大约是广播超时的一半(5秒左右),如果你没在5秒内调用finish(),系统仍会判定ANR。
// 错误的做法:直接在onReceive里做耗时操作
@Override
public void onReceive(Context context, Intent intent) {
// 可能阻塞主线程 → ANR!
doHeavyWork();
}
// 正确的做法:使用goAsync
@Override
public void onReceive(Context context, Intent intent) {
PendingResult pendingResult = goAsync();
new Thread(() -> {
doHeavyWork();
pendingResult.finish();
}).start();
}
三种超时的共同根源
聊到这,你可能会问:“为什么对象泄漏会导致这些超时?” 我解释一下:
- 对象泄漏 → 内存不足 → 频繁GC:GC时会暂停所有线程(STW),如果主线程恰好被GC卡住,5秒超时很容易被突破。
- 对象泄漏 → 持有Activity引用 → 锁无法释放:比如一个静态集合持有了Activity,Activity又持有一个锁,其他线程想拿锁就得等Activity销毁,但Activity销毁不了……死锁了。
- 对象泄漏 → Handler消息积压:泄漏的Handler不断往主线程发消息,队列被撑满,新事件排不上队。
/data/anr/traces.txt,看看主线程的堆栈停在哪个锁或哪个方法上。95%的ANR都能从这里找到线索。
总结一下:三种超时的本质都是主线程被阻塞。阻塞的原因千千万,但对象泄漏是高频元凶。理解了触发机制,我们才能对症下药。下一期,我们专门聊聊怎么用LeakCanary和MAT定位泄漏对象。