ANR原理与类型:输入事件超时、服务超时、广播超时等触发机制

ANR,全称 Application Not Responding,就是应用无响应。说白了,当主线程被长时间阻塞,系统就会弹出那个烦人的对话框:「××应用无响应,是否继续等待?」

我接手过不少ANR案例,大部分都跟对象泄漏直接相关。但想根治,得先搞清楚系统到底是怎么判定ANR的。今天我们就聊聊三种经典触发机制:输入事件超时、服务超时、广播超时。

核心触发机制总览

系统通过几个核心组件来监控主线程的响应能力。一旦超时阈值被突破,ANR就发生了。下面这张图可以帮你快速建立认知框架:

ANR触发机制与超时阈值 ANR触发 输入事件超时 (5秒) 服务超时 (20秒) 广播超时 (10秒) Touch/Key事件未消费 主线程Looper阻塞 Service#onStartCommand超时 onReceive执行超时 goAsync()未及时响应 ⚠ 核心要点 三种超时都监测主线程是否被阻塞。对象泄漏常导致主线程长期持锁或无限GC。 你想想看,如果某个对象泄漏导致大量Bitmap没有释放,GC线程频繁Full GC,主线程能不卡吗?

输入事件超时 —— 最常见的ANR

Android系统规定,如果应用在 5秒 内没有对输入事件(触摸、按键)作出响应,系统就会判定ANR。这个5秒从事件被派发到主线程开始算,直到处理完并反馈完成。

我在项目中遇到过几次,明明代码里没有死循环,却频繁弹出ANR。后来一查,是主线程在等待某个锁,而那个锁被一个泄漏的Activity持有。

避坑指南: 我曾经在调试一个即时通讯App时发现,用户连续快速点击“发送”按钮时,经常出现ANR。原因是每次点击都创建一个新的HandlerThread去处理网络,但旧线程没销毁,大量Message积压在主线程的MessageQueue里。
解决办法很简单:加一个阀门,控制并发数量。

服务超时 —— 容易被忽视的“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就出现了。

重要提示: 如果你的广播处理需要超过10秒,必须调用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();
}

三种超时的共同根源

聊到这,你可能会问:“为什么对象泄漏会导致这些超时?” 我解释一下:

  1. 对象泄漏 → 内存不足 → 频繁GC:GC时会暂停所有线程(STW),如果主线程恰好被GC卡住,5秒超时很容易被突破。
  2. 对象泄漏 → 持有Activity引用 → 锁无法释放:比如一个静态集合持有了Activity,Activity又持有一个锁,其他线程想拿锁就得等Activity销毁,但Activity销毁不了……死锁了。
  3. 对象泄漏 → Handler消息积压:泄漏的Handler不断往主线程发消息,队列被撑满,新事件排不上队。
个人技巧: 我一般会在ANR发生时,第一时间拉取 /data/anr/traces.txt,看看主线程的堆栈停在哪个锁或哪个方法上。95%的ANR都能从这里找到线索。

总结一下:三种超时的本质都是主线程被阻塞。阻塞的原因千千万,但对象泄漏是高频元凶。理解了触发机制,我们才能对症下药。下一期,我们专门聊聊怎么用LeakCanary和MAT定位泄漏对象。