ANR 基础:到底什么是 ANR?

聊后台任务超时和帧率崩溃之前,咱们得先把 ANR 这件事彻底搞明白。很多人做了几年 Android 开发,一遇到 ANR 就只会看 log,然后凭感觉猜。说实话,我也干过这种事。

我记得有一次线上突然爆了一堆 ANR,用户反馈 app 点不动,过一会儿就弹出“应用无响应”。我当时第一反应是去查主线程耗时操作,结果查了一圈发现线程栈里干干净净,主线程根本没卡。后来才意识到——哦,原来是广播超时了。

这个例子让我养成了一个习惯:遇到 ANR,先问自己三个问题——它是什么类型?超时时间是多久?哪个组件触发的?今天咱们就从这开始,把 ANR 的根儿刨清楚。

ANR 的定义:系统在等你,但你等得太久了

ANR 全称 Application Not Responding,说白了就是系统给你发了一条消息,你超过规定时间还没处理完,系统就觉得你死掉了,于是弹个对话框告诉用户:这个应用没反应,要不要关掉?

为什么系统要干这事儿?很简单,Android 要保证用户操作的流畅性。你想想看,用户点了个按钮,结果界面卡了 10 秒钟,这体验谁受得了?所以系统给每个组件都设了超时门槛。

避坑提示: 我曾经见过新手在 BroadcastReceiver 里直接执行网络请求,结果 10 秒不到就被系统 kill 了。记住:只要是主线程,就别干重活。

ANR 的四种类型:谁超时了?

Android 系统定义了四种常见的 ANR 类型。每种类型的超时时间、触发条件都不一样,咱们逐个过。

类型 超时时间 触发条件 典型场景
输入调度超时
InputDispatchTimeout
5 秒 主线程未处理完 input 事件(如触摸、按键) 列表滑动时卡顿、点击无响应
广播超时
BroadcastTimeout
前台 10 秒 / 后台 60 秒 BroadcastReceiver 的 onReceive() 未在规定时间内返回 接收开机广播后做大量初始化
服务超时
ServiceTimeout
前台 20 秒 / 后台 200 秒 Service 的 onCreate()、onStartCommand()、onBind() 执行时间超限 Service 中执行大文件写入
ContentProvider 超时
ContentProviderTimeout
10 秒 ContentProvider 的 query()、insert()、update()、delete() 执行超时 Provider 中查询大量数据库数据

输入调度超时:最容易被用户感知的 ANR

这种 ANR 直接跟用户操作挂钩。你点了一个按钮,系统把事件派发给主线程的 Looper,如果 5 秒内没有处理完,InputDispatcher 就会认为应用挂了。

// 常见例子:主线程 sleep 了
@Override
public void onClick(View v) {
    try {
        Thread.sleep(6000);  // 超过5秒,必出ANR
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

这里我特别想提醒:你以为只有 sleep 才算?IO 操作、大的 Bitmap 解码、锁等待……主线程上任何超过 5 秒的操作都会触发输入调度超时。

广播超时:悄无声息的杀手

广播 ANR 常常藏得很深。前台广播只有 10 秒,后台广播虽然有 60 秒,但 Android 8.0 以后对后台广播做了严格限制,你甚至可能等不到超时就先被杀死了。

我在项目里遇到过最坑的一次是:一个三方 SDK 在静态注册的广播里做了网络请求,后台 60 秒超时看起来够用,但实际上网络波动加上重试逻辑,用户等了一分多钟才弹出 ANR。最后排查下来,SDK 把 goAsync() 用成了同步模式。

注意: 使用 goAsync() 虽然可以把耗时操作移到子线程,但你必须保证在 onReceive() 返回后 10 秒(前台)或 60 秒(后台)内调用 PendingResult.finish()。否则 ANR 依然会报。

服务超时:后台逻辑的定时炸弹

Service 的 ANR 只有在主线程执行 onCreate、onStartCommand、onBind 这些生命周期方法时才会触发。很多人觉得 Service 本身就是后台的,所以直接在里面写耗时操作。这是大忌——Service 默认跑在应用的主线程里。

// 错误的做法:Service 主线程直接做耗时操作
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    // 假设这里做了数据库批量插入,花了30秒
    database.bulkInsert(bigData);
    return START_STICKY;
}

正确的姿势:要么用 IntentService,要么手动起工作线程,要么用 JobScheduler 或 WorkManager。

ContentProvider 超时:跨进程的憋屈

这种 ANR 比较特殊,它往往发生在跨进程访问 Provider 的时候。比如另一个 app 通过 Provider 查询你的数据,你的 Provider 中如果做了慢查询(比如全表扫描),就会触发 10 秒超时。

我记得有一次系统联系人应用频繁 ANR,大量用户反映通讯录打不开。最后发现是我们自建的表没有索引,每次查询都要全表扫描 10 万条数据,10 秒根本跑不完。加了索引之后,问题立刻消失。

关键指标: Provider 的 query() 方法必须保证在 10 秒内返回。如果数据量大,分批返回或者用 CursorLoader 配合 ContentResolver 异步查询是更好的选择。
Android ANR 类型框架 ANR (应用无响应) 输入调度超时 5秒未处理Input事件 广播超时 前台10s/后台60s 服务超时 前台20s/后台200s ContentProvider超时 10秒未返回查询/写入 📌 说明: 超时时间均为 Android 系统默认值,不同厂商可能略有调整。 所有 ANR 类型都要求主线程(UI线程)在限定时间内完成响应,否则系统会弹出 ANR 对话框。

总结与避坑

现在你大概明白了吧?ANR 不是玄学,它就是系统给你设的四个超时警戒线。我整理了三个特别容易踩的坑:

  • Input ANR: 别在主线程做任何超过 5 秒的事,包括 SharedPreferences 的跨进程读写——它可能导致锁等待。
  • Broadcast ANR: 静态注册的广播 receiver 中不要直接用 Context.startService() 或网络请求,用 goAsync() 并配好执行线程。
  • Service ANR: 永远不要在 Service 的生命周期方法里直接跑耗时逻辑,尤其 onStartCommand。
我的习惯: 每次提交代码前,我都会用 StrictMode 跑一遍,检查主线程上有没有潜在的磁盘 IO 和网络操作。虽然不能完全替代 ANR 检测,但能提前发现 80% 的问题。