ANR类型详解:输入事件超时、广播超时、服务超时、ContentProvider超时

ANR 其实就四种类型,搞懂它们各自的超时机制,排查问题才能快准狠。我刚开始接触 ANR 时,总以为只有输入事件才会触发,后来踩了广播和服务的坑,才发现每种类型都有自己的脾气。

ANR 类型与超时机制 ANR 四大类型 输入事件超时 超时:5s 场景:UI线程阻塞 例如:触摸无响应 广播超时 前台:10s 后台:60s onReceive() 耗时 服务超时 超时:20s onStartCommand() 或 onCreate() 超时 ContentProvider超时 超时:10s query/insert 等 操作主线程 所有超时都由系统监控线程(Watchdog)检测,触发后弹出 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 的常见诱因之一。嗯,我们先打好基础,后面章节会深入具体案例。