4. 广播超时 ANR:BroadcastReceiver onReceive 执行超过 10 秒

广播超时 ANR 是最常见也最容易踩坑的一类。说得直白点,系统给了你 10 秒时间,你的 onReceive 没跑完,它就认为你卡死了,直接弹 ANR 对话框。

我知道很多同学第一反应:「我 onReceive 里只是调了个接口啊,怎么可能卡 10 秒?」嗯,这里有个误区——你调的是后台线程还是主线程?你调完接口有没有等回调?回调回来会不会再卡一下?现实比想象复杂得多。

核心规则: onReceive 默认运行在 主线程,且系统给前台广播的 timeout 是 10 秒,后台广播是 60 秒。前台广播一旦超时,必出 ANR。

4.1 触发场景:哪些操作会把 onReceive 堵住 10 秒?

我个人经历里,最常见的有四大类。咱们一个一个拆。

4.1.1 直接在 onReceive 里做网络请求

这几乎是新手翻车率最高的操作。你写了个 HttpURLConnection.connect(),或者用 Retrofit 但忘了切线程,直接把同步调用丢在主线程。如果网络慢、后端超时,10 秒一眨眼就过去了。我曾经接手过一个项目,有人在广播里下载一个 5MB 的配置文件,下载逻辑直接写在 onReceive 里,结果每分钟被系统杀一次。

4.1.2 持锁导致主线程等待

广播的 onReceive 可能需要获取某个锁(比如 SharedPreferences 的写入锁、数据库的写锁),而另一个线程恰好在长时间持有这个锁。比如你在子线程批量写数据库,主线程的 onReceive 也想读,就会被堵住。我在排查一个视频 App 的 ANR 时,发现是崩溃上报库的线程锁住了 SQLite 的 WAL 文件,广播根本拿不到连接。那叫一个郁闷。

4.1.3 广播串行排队——系统广播的「插队」效应

所有显式广播默认是串行分发的。如果前一个广播的 onReceive 还没返回,你的广播就得排队等。尤其是很多三方 SDK 都喜欢注册系统广播(比如网络变化、屏幕解锁),它们自己写得慢,直接把你拖下水。你想想看,你只是监听一个自定义广播,结果前面有三四个三方库的 Receiver 各卡 3 秒,你家广播根本跑不完。

4.1.4 主线程 Looper 卡住——「城门失火,殃及池鱼」

有时 onReceive 本身并不慢,但主线程的消息队列里前面塞了一堆任务(比如布局渲染、动画更新)。广播消息排在队尾,等到它执行时已经过了 8 秒,然后在 onReceive 里稍微一动就超时了。这种情况非常隐蔽,只看 onReceive 代码看不出问题。

4.2 排查方法:从 traces 到代码,三步定位

排查广播 ANR 其实有一套固定套路。我建议你按顺序走,不要乱翻代码。

4.2.1 第一步:拿到 ANR traces 文件

发生 ANR 后,系统会在 /data/anr/traces.txt 里记录堆栈。你只要看主线程的 stack,找到 BroadcastReceiver.onReceive 那个调用链,就能知道它在哪行代码卡住了。

// 典型的 ANR traces 片段(主线程堆栈)
"main" prio=5 tid=1 Native
  at android.os.MessageQueue.nativePollOnce(Native Method)
  at android.os.MessageQueue.next(MessageQueue.java:323)
  at android.os.Looper.loop(Looper.java:136)
  at android.app.ActivityThread.main(ActivityThread.java:xxxx)
  at java.lang.reflect.Method.invoke(Native Method)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(...)
  at com.android.internal.os.ZygoteInit.main(...)
  // 重点:下面这行是你广播的 onReceive
  - waiting on <0x...> (a java.lang.Object)   // 可能是在等锁
  at com.example.MyReceiver.onReceive(MyReceiver.java:25)
小技巧: 如果 traces 里没有你的 receiver 堆栈,别慌。再查查 “waiting on” 后面的对象,看看谁持有了那个锁。用 dumpsys activity broadcasts 也可以看到当前广播队列的状态。

4.2.2 第二步:结合 logcat 过滤 ANR 日志

用 adb logcat -b events | grep "broadcast" 可以看到系统分发的每个广播的耗时。尤其关注 finishReceiver 的时间戳,如果距离 scheduleBroadcast 超过 10 秒,那就是你。

我记得有一次帮同事排查,他怀疑是三方推送库的广播超时,结果 logcat 显示他自家 Receiver 耗时 12 秒——里面有个 for 循环做了大量字符串拼接。他说「不可能啊,我用的 StringBuilder」,我说「你 StringBuilder 也经不起循环 10 万次啊」。

4.2.3 第三步:分析是否被其他 Receiver 拖累

使用 dumpsys activity broadcasts 可以列出广播队列中所有待处理的和正在处理的 Receiver,按注册顺序排列。如果发现你的 Receiver 前面有很多等待项,说明是排队问题。这时候你需要排查前面那些 Receiver 为什么慢,或者考虑把你的广播改成异步使用 goAsync()。

4.3 解决方案:从根上避免广播 ANR

场景解决方案优先级
onReceive 内有耗时 I/O 使用 goAsync() 获取 PendingResult,在子线程完成后调用 finish() 高
持锁冲突 onReceive 内避免获取锁;如果必须拿锁,用 tryLock 并给超时 中
被系统广播排队拖累 使用动态注册 + IntentFilter.setPriority() 无效;改为用 LocalBroadcastManager 或 LiveData 通信 高
主线程 Looper 已卡 优先排查主线程其他任务,使用 BlockCanary 或 Choreographer 监控帧率 低
注意: goAsync() 并不是银弹。它只是把 onReceive 的执行上下文从主线程剥离,但你仍然需要在子线程里保证在 10 秒内调用 finish(),否则同样超时。我曾经见过有人用了 goAsync(),结果在子线程里做了一个网络重试 3 次的逻辑,每次重试间隔 5 秒——好家伙,15 秒过去了,ANR 照样弹。

4.3.1 正确使用 goAsync() 的样板

public class MyReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 先检查是否允许异步
        final PendingResult pending = goAsync();
        if (pending == null) {
            // 系统不支持异步(Android 8.0 以下某些情况)
            // 什么也别做,直接 return
            return;
        }
        // 丢到子线程
        new Thread(() -> {
            try {
                // 做你的事,但一定要设定全局超时
                doSomething(context, intent);
            } finally {
                // 千万不要忘了调 finish()
                pending.finish();
            }
        }).start();
    }
}
关键点: goAsync() 必须在 onReceive 方法体内最前面调用,且不能跨多个 PendingResult。一个 Receiver 只能调一次 goAsync(),否则会抛异常。

4.4 我的避坑清单

  • 不要将网络请求直接写在 onReceive 中——就算用 goAsync(),也要加业务超时(不要依赖系统 10 秒,自己设 5 秒就足够)。
  • 小心 SharedPreferences 的 commit()——它在主线程写磁盘,改用 apply() 异步提交。
  • 排查广播排队问题——如果 ANR 日志里没你的代码堆栈,多半是被别人拖累。用 adb shell dumpsys activity broadcasts 看“历史广播”列表,找到最慢的那个。
  • 我曾经踩过的坑:在广播里调用 startActivity() 启动一个冷启动特别慢的 Activity —— 没有 ANR,但 onReceive 返回了,Activity 的生命周期耗时会占用主线程,导致下一个广播超时。不要觉得这很离谱,我实际遇到过两次。
广播ANR排查流程图 收到ANR弹窗 第一步:查看 /data/anr/traces.txt 主线程堆栈是否有 onReceive? 有 查看堆栈:具体哪一行卡住 检查耗时操作 / 锁等待 无 排查广播排队问题 dumpsys & logcat 解决方案:goAsync() / 锁优化 / 排队排查 避免主线程耗时操作,合理设置业务超时 关键日志命令 adb logcat -b events | grep "broadcast" | adb shell dumpsys activity broadcasts

4.5 总结一句话

广播超时 ANR 本质上是主线程被占了 10 秒以上。你不需要背一堆理论,只要记住:onReceive 里不干重活,真需要干就用 goAsync() 加超时控制。排查时先从 traces 文件看主线程堆栈,没有就查广播排队——这套路我用了四五年,没失手过。