ANR类型详解:输入事件超时、广播超时、服务超时、ContentProvider超时
ANR 其实就四种类型,搞懂它们各自的超时机制,排查问题才能快准狠。我刚开始接触 ANR 时,总以为只有输入事件才会触发,后来踩了广播和服务的坑,才发现每种类型都有自己的脾气。
一、输入事件超时(InputEvent Timeout)
说白了就是用户点了屏幕,系统等你的 UI 线程处理,等了 5 秒还没回应。嗯,这是我的心理底线,也是 Android 的底线。
触发条件很简单:主线程在 5 秒内没有处理完输入事件(触摸、按键等)。每次你用手指戳屏幕,系统会抛一个消息到主线程的 Looper 队列里。如果队列前面卡了个耗时操作,消息就一直排不上队。
典型场景:
- 主线程做网络请求(别笑,我在遗留项目里见过)
- 复杂布局测量(尤其是 RecyclerView 嵌套)
- 磁盘 I/O 写日志,没用异步
- Bitmap 解码没走子线程
我记得有一次线上反馈 app 点按钮要等 6 秒才有反应。查 log 发现主线程正在 decode 一张 4K 图片。改到 Glide 异步加载后就解决了。
二、广播超时(BroadcastReceiver Timeout)
广播分两种:前台广播和后台广播。它们的超时差异很大,我建议你背下来:
| 广播类型 | 超时时间 | 是否会影响其他广播 |
|---|---|---|
| 前台广播(通过 Intent 发送,不带 FLAG_RECEIVER_FOREGROUND) | 10 秒 | 会阻塞同优先级广播 |
| 后台广播(通过 Manifest 静态注册或系统自动分发) | 60 秒 | 随机被系统 kill |
为什么前台只有 10 秒?因为前台广播跑在系统分发线程里,卡太久整个系统的 Broadcast 队列都会被堵住。我在项目中就吃过这个亏:自定义广播里做了数据库批量插入,结果别的广播全挂了,直接引发 ANR。
我的习惯: onReceive() 里只用 goAsync() 或者丢给后台 Service,不自己搞耗时任务。
代码示例——合法使用广播的姿势:
public class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 获取 PendingResult 并异步执行
final PendingResult result = goAsync();
AsyncTask.execute(() -> {
// 耗时操作在这里做
doHeavyWork();
result.finish(); // 一定要调 finish()
});
}
}
警告: 如果不用 goAsync() 且代码超过 10 秒/60 秒,系统直接弹出 ANR。而且 onReceive() 里不要创建 Handler,它没有 Looper。
三、服务超时(Service Timeout)
Service 的 ANR 比较隐蔽。它不是在 Service 运行过程中超时,而是 启动阶段 超时。具体说:当 startService() 或 bindService() 调用后,系统会给 Service 一个 20 秒的时间窗口,让你完成 onCreate() 和 onStartCommand()(或者 onBind())。如果超过 20 秒还没返回,就炸了。
我曾经在 onCreate() 里初始化一个网络 SDK,它内部做了同步 DNS 解析,偶尔卡住 30 秒。用户反馈 app 打开后几秒就白屏,然后弹 ANR。查 log 才发现是 Service 超时。
避坑指南:
- Service 的
onCreate()只做轻量初始化,比如赋值、注册监听 - 耗时初始化扔到子线程,用
startForeground()保活 - 如果 Service 需要执行长期任务,使用
IntentService或JobIntentService
顺便提一句,IntentService 内部自带子线程 Handler,超时风险大大降低。不过 Android 8.0 开始推荐用 JobScheduler 了。
四、ContentProvider 超时(ContentProvider Timeout)
这个最容易被忽略。ContentProvider 的 ANR 发生在其他进程访问你的 Provider 时,如果主线程阻塞超过 10 秒,系统就会向访问方进程弹出 ANR。注意:是访问方 ANR,不是你 Provider 所在进程。
举个真实案例:我的 app 有个 Provider 暴露给外部应用,结果它的 query() 方法直接在主线程做了全表 left join 查询,数据量 5 万行。外部调用的 app 每调一次就 ANR。我当时的解决方案是:
- 用
CursorLoader或子线程执行查询 - 使用
ContentResolver.query()时加上cancellationSignal - 对于大数据量,添加分页参数
| 方法 | 超时时间 | 超时影响方 |
|---|---|---|
| query | 10s | 调用方进程 |
| insert | 10s | 调用方进程 |
| update | 10s | 调用方进程 |
| delete | 10s | 调用方进程 |
你想想看,如果你的 Provider 自己进程的主线程正忙,而外部进程恰好调用你的 Provider——就等着被对方吐槽吧。所以 Provider 的 CRUD 方法里千万不要阻塞主线程。
四种 ANR 总结对比
我习惯用一张表记住它们的关键差异:
| 类型 | 超时时间 | 触发条件 | 受影响进程 |
|---|---|---|---|
| 输入事件 | 5 秒 | 主线程未处理完输入事件 | 本进程 |
| 前台广播 | 10 秒 | onReceive() 未返回 | 系统广播队列等待 |
| 后台广播 | 60 秒 | onReceive() 未返回 | 系统可能杀进程 |
| 服务 | 20 秒 | onCreate/onStartCommand 未返回 | 本进程 |
| ContentProvider | 10 秒 | CRUD 方法在主线程超时 | 调用方进程 |
个人建议: 遇到 ANR 先看 logcat 里的 "ANR in" 关键字,它会直接告诉你类型。然后去 /data/anr/traces.txt 里找主线程堆栈,看是卡在哪个阶段。养成这个习惯,排查速度能快一倍。
其实搞懂这四种 ANR 的原理,后面 Room 优化就顺多了——比如 Room 在主线程执行查询,本质上就是输入事件 ANR 的常见诱因之一。嗯,我们先打好基础,后面章节会深入具体案例。