BroadcastReceiver 超时:onReceive 执行限制与阈值
BroadcastReceiver 的 ANR,说实话,是我早期做 Android 开发时踩得比较多的坑之一。你想想看,一个广播发出去,onReceive 里做了个网络请求或者数据库写操作,结果十几秒没返回 —— 系统直接弹 ANR 对话框。用户看到那个对话框,基本就该卸载 App 了。
这一章我们专门聊聊 BroadcastReceiver 的超时机制。我会把我这些年踩过的坑、总结的经验都摊开来说。
onReceive 的执行限制 —— 为什么不能做耗时操作?
BroadcastReceiver 的 onReceive 方法运行在哪儿?主线程。这一点特别关键。我见过不少开发者在 onReceive 里直接做网络请求、写数据库、甚至执行大文件复制 —— 这其实是在玩火。
系统给 onReceive 设定的超时时间其实很短。前台广播只有 10 秒,后台广播稍微长点,60 秒。一旦超时,系统就会判定这个 Receiver 无响应,直接 ANR。
关键事实:onReceive 返回后,系统才会认为广播处理完毕。如果你在 onReceive 里启动了新线程做耗时操作,但 onReceive 本身很快返回了 —— 这样不会超时。但如果你把耗时操作直接写在 onReceive 的主路径上,那每一毫秒都在消耗超时倒计时。
我自己的习惯是:onReceive 里只做两件事 —— 1)解析 Intent 中的数据;2)通过 goAsync 或启动 Service/JobScheduler 把实际工作交出去。
goAsync —— 唯一的官方异步方案
其实 Android 官方提供了一个叫 goAsync() 的机制。这个东西有点冷门,但非常实用。不绕弯子,直接看代码:
public class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 1. 调用 goAsync,获得 PendingResult
final PendingResult pendingResult = goAsync();
// 2. 在后台线程做实际工作
new Thread(() -> {
try {
// 模拟耗时操作
Thread.sleep(5000);
// 处理逻辑...
} finally {
// 3. 必须调用 finish() 通知系统
pendingResult.finish();
}
}).start();
// onReceive 立即返回,不阻塞主线程
}
}
这里有个陷阱我必须要提醒你:goAsync() 虽然能把工作挪到后台线程,但系统给这个异步任务的超时时间 并没有延长。前台广播还是 10 秒,后台广播还是 60 秒。如果你在后台线程里做了 20 秒的操作,而这是一个前台广播 —— 照 ANR 不误。
我曾经踩过这个坑:有个版本上线后,用户反馈偶尔出现 ANR。我查了很久才发现,同事在 goAsync 的线程里做了一次网络请求,网络不稳定时耗时超过 10 秒。前台广播 + 10 秒阈值,直接命中。后来我们改成了启动一个 Foreground Service 来处理这个请求,问题才彻底解决。
异步广播 vs 同步广播 —— 本质区别在哪?
很多新人搞不清这两种广播的区别。我这里用一句话说清楚:
- 同步广播(普通广播):多个 Receiver 按顺序执行,前一个 Receiver 的 onReceive 返回后,下一个才执行。阻塞的,串行的。
- 异步广播(sendBroadcast):系统把广播发给所有匹配的 Receiver,每个 Receiver 在自己的主线程上独立处理,互不等待。非阻塞的,并行的。
但注意,这里说的「异步」只是 Receiver 之间的相互不阻塞,并不代表 onReceive 本身是异步的。每个 Receiver 的 onReceive 仍然在自己的主线程上跑,仍然受超时限制。
还有一种特殊的 有序广播(ordered broadcast),它也是同步的,但 Receiver 之间可以按优先级传递结果,并且可以截断广播。这个机制在系统广播里用得比较多,比如短信拦截。
| 广播类型 | Receiver 执行方式 | 是否阻塞 | 典型场景 |
|---|---|---|---|
| 普通广播(同步) | 串行,按顺序 | 是 | 系统通知、应用内事件 |
| 异步广播 | 并行,各自独立 | 否 | 不需要等待结果的场景 |
| 有序广播 | 串行,按优先级 | 是 | 短信拦截、电话拦截 |
超时阈值 —— 前台 10 秒,后台 60 秒,但别卡着底线
不同 Android 版本下,超时阈值其实有过微调。但当前主流版本(Android 12 ~ 15)的通用规则是:
- 前台广播(Foreground broadcast):超时时间 10 秒
- 后台广播(Background broadcast):超时时间 60 秒
这里说的「前台」和「后台」,指的是发送广播时 App 是否处于可见状态。如果 App 在后台,系统会给更长的超时时间,但别因此放松警惕 —— 60 秒干不完的活多了去了。
Android 15 开始,系统对广播超时做了更严格的限制。后台广播的超时时间从 60 秒缩短到 30 秒,而且系统会检测 Receiver 是否调用了 goAsync 但没有及时 finish。如果你的 App 要适配 Android 15 以上,这部分一定要仔细检查。
我个人的建议:不要把超时时间当作「安全区」。前台广播按 5 秒以内规划,后台广播按 15 秒以内规划。留出余量,运维不慌。你想想看,用户手机可能正在卡顿、CPU 被其他 App 占满、IO 在排队 —— 你的 10 秒操作实际可能跑成 20 秒。
避坑指南 —— 我踩过的几个真实案例
案例 1:曾经有一个版本,我们在 onReceive 里直接调了一个 SDK 的同步初始化方法。那个方法内部做了网络请求 + 插件加载,耗时不稳定。线上 ANR 率飙升到 0.8%。排查下来,广播超时占了 70%。解决方案是把初始化改成懒加载,onReceive 只发一个 EventBus 事件,让业务层自己去初始化。
案例 2:另一个项目里,同事用了 goAsync 但忘了调用 finish()。结果系统一直等这个 Receiver 结束,直到超时。这种 bug 特别隐蔽,因为不是每次都触发,只在系统负载高的时候才出现。我们后来加了一套检查机制 —— 在 Receiver 里用 Handler 做兜底,超过 8 秒还没 finish 就自动打 log 并强制 finish。
案例 3:有序广播的截断问题。我们在系统通话状态广播里做逻辑处理,结果另一个 App 的高优先级 Receiver 截断了广播,我们这边根本收不到。解决方案是改用动态注册 + 设置最高优先级,同时加了一个兜底的轮询机制。
总结一下重点
- onReceive 运行在主线程,不能做耗时操作 —— 这是铁律
- goAsync 可以把工作挪到后台线程,但超时时间不变
- 同步广播串行阻塞,异步广播并行不阻塞,有序广播按优先级传递
- 前台广播超时 10 秒,后台广播超时 60 秒(Android 15 后台改为 30 秒)
- 每个 Receiver 必须调用 finish(),否则系统会一直等待
嗯,BroadcastReceiver 的超时问题其实就这些核心要点。但说实话,越简单的东西越容易出问题 —— 因为大家都不重视。我建议你把项目里所有 Receiver 都过一遍,看看 onReceive 里有没有直接做耗时操作的,有的话赶紧改。