4. 广播超时 ANR:BroadcastReceiver onReceive 执行超过 10 秒
广播超时 ANR 是最常见也最容易踩坑的一类。说得直白点,系统给了你 10 秒时间,你的 onReceive 没跑完,它就认为你卡死了,直接弹 ANR 对话框。
我知道很多同学第一反应:「我 onReceive 里只是调了个接口啊,怎么可能卡 10 秒?」嗯,这里有个误区——你调的是后台线程还是主线程?你调完接口有没有等回调?回调回来会不会再卡一下?现实比想象复杂得多。
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)
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 监控帧率 | 低 |
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();
}
}
4.4 我的避坑清单
- 不要将网络请求直接写在 onReceive 中——就算用 goAsync(),也要加业务超时(不要依赖系统 10 秒,自己设 5 秒就足够)。
- 小心 SharedPreferences 的 commit()——它在主线程写磁盘,改用 apply() 异步提交。
- 排查广播排队问题——如果 ANR 日志里没你的代码堆栈,多半是被别人拖累。用
adb shell dumpsys activity broadcasts看“历史广播”列表,找到最慢的那个。 - 我曾经踩过的坑:在广播里调用
startActivity()启动一个冷启动特别慢的 Activity —— 没有 ANR,但 onReceive 返回了,Activity 的生命周期耗时会占用主线程,导致下一个广播超时。不要觉得这很离谱,我实际遇到过两次。
4.5 总结一句话
广播超时 ANR 本质上是主线程被占了 10 秒以上。你不需要背一堆理论,只要记住:onReceive 里不干重活,真需要干就用 goAsync() 加超时控制。排查时先从 traces 文件看主线程堆栈,没有就查广播排队——这套路我用了四五年,没失手过。