ANR 基础:到底什么是 ANR?
聊后台任务超时和帧率崩溃之前,咱们得先把 ANR 这件事彻底搞明白。很多人做了几年 Android 开发,一遇到 ANR 就只会看 log,然后凭感觉猜。说实话,我也干过这种事。
我记得有一次线上突然爆了一堆 ANR,用户反馈 app 点不动,过一会儿就弹出“应用无响应”。我当时第一反应是去查主线程耗时操作,结果查了一圈发现线程栈里干干净净,主线程根本没卡。后来才意识到——哦,原来是广播超时了。
这个例子让我养成了一个习惯:遇到 ANR,先问自己三个问题——它是什么类型?超时时间是多久?哪个组件触发的?今天咱们就从这开始,把 ANR 的根儿刨清楚。
ANR 的定义:系统在等你,但你等得太久了
ANR 全称 Application Not Responding,说白了就是系统给你发了一条消息,你超过规定时间还没处理完,系统就觉得你死掉了,于是弹个对话框告诉用户:这个应用没反应,要不要关掉?
为什么系统要干这事儿?很简单,Android 要保证用户操作的流畅性。你想想看,用户点了个按钮,结果界面卡了 10 秒钟,这体验谁受得了?所以系统给每个组件都设了超时门槛。
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 秒根本跑不完。加了索引之后,问题立刻消失。
query() 方法必须保证在 10 秒内返回。如果数据量大,分批返回或者用 CursorLoader 配合 ContentResolver 异步查询是更好的选择。
总结与避坑
现在你大概明白了吧?ANR 不是玄学,它就是系统给你设的四个超时警戒线。我整理了三个特别容易踩的坑:
- Input ANR: 别在主线程做任何超过 5 秒的事,包括
SharedPreferences的跨进程读写——它可能导致锁等待。 - Broadcast ANR: 静态注册的广播 receiver 中不要直接用
Context.startService()或网络请求,用goAsync()并配好执行线程。 - Service ANR: 永远不要在 Service 的生命周期方法里直接跑耗时逻辑,尤其
onStartCommand。
StrictMode 跑一遍,检查主线程上有没有潜在的磁盘 IO 和网络操作。虽然不能完全替代 ANR 检测,但能提前发现 80% 的问题。