先把结论放在前面:BroadcastReceiver 的答案分三层——"能收到广播"是 API 层、"知道哪些广播被限制"是机制层、"知道替代方案是什么"是工程层。 Android 8 之后隐式广播被大幅限制、9 之后后台广播进一步收紧,能答到第三层的人才算真懂。
BroadcastReceiver 看起来是个简单组件,追问起来全是版本变化和限制规则。这篇按"是什么 → 怎么工作 → 限制在哪 → 怎么办"四步讲清。
两种注册,两条投递路径
静态注册写在清单里,由系统在应用启动时(ContentProvider 初始化之后、Application.onCreate 之前)建立接收器,进程被杀时也能收到——但受后台广播限制。
<receiver android:name=".NetworkChangeReceiver" android:exported="false">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
动态注册通过 registerReceiver 在运行时建立,必须与注册所在的 Context 生命周期对应——Activity 上注册要 unregisterReceiver,否则泄漏。
class MainActivity : AppCompatActivity() {
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// 注意:onReceive 跑在主线程
handleNetworkChange(intent)
}
}
override fun onStart() {
super.onStart()
// API 33+ 必须显式传导出标志
registerReceiver(receiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION),
Context.RECEIVER_NOT_EXPORTED)
}
override fun onStop() {
super.onStop()
unregisterReceiver(receiver)
}
}
跨 API 的注册写法要注意:ContextCompat.registerReceiver(...) 已经在新 androidx 里统一处理了导出标志的兼容问题,比直接调 registerReceiver 更稳。
最常见的坑是
onReceive 里做耗时操作,超过前台广播时限触发 ANR。
onReceive 在主线程执行,系统对前台广播给了约 10 秒窗口,超时会 ANR;后台广播窗口更短(约 30 秒,且从 Android 8 起很多已不再投递)。有人在这里直接发起网络请求、写数据库、解析大 JSON,结果是 UI 卡顿甚至无响应,而日志里看不出是哪次广播触发的。
规范写法是把耗时部分转出去:
override fun onReceive(context: Context, intent: Intent) {
// 只做轻量判断 + 转交,onReceive 尽快返回
val pending = goAsync()
scope.launch {
try {
syncData(intent.getStringExtra(EXTRA_ID))
} finally {
pending.finish() // 必须调,否则进程可能被判定异常
}
}
}
goAsync() 返回 PendingResult,处理完必须 finish()。但要注意它的约束:进程仍可能在 finish() 之前被杀,所以它是"提高成功率"而不是"保证成功"。真正需要保证完成的工作应该交给 WorkManager 或前台服务。
还有一个更隐蔽的坑:用 BroadcastReceiver 的经验法则是前提显式声明,兜底显式实现,上线前少出事。落到具体动作是三条。① 能用显式广播就不发隐式——setPackage 或 setComponent 显式指定目标,避免被别的应用截获;② 敏感操作用权限保护——自定义 action 要配 <permission> 并在发送方声明,否则任何应用都能伪造;③ 注册要收敛——不要用动态注册长期监听高频广播(ACTION_SCREEN_ON、BATTERY_CHANGED),这类改用 ContentObserver 或 SensorManager 更合适。
还有三个话题值得单独说,因为它们是"用过"与"用明白"的分界线。
话题一:有序广播与结果回传。 orderedBroadcast 让接收者按优先级依次处理,每个接收者可以用 setResultCode 与 setResultData 传回结果,最终调用方通过 sendOrderedBroadcast(..., resultReceiver, ...) 拿到。但这条路在现代开发里已经不推荐:有序广播会阻塞、无法取消、优先级在后台限制下形同虚设。需要"请求-响应"式交互时,正确的做法是 Messenger、绑定(AIDL)或单进程内的直接调用。
话题二:本地广播。 LocalBroadcastManager 曾用于应用内广播(不进系统、不被其它应用收到),但它已在 AndroidX 里标记废弃。原因是它解决的是"不想要外部收到"这个需求,而这个需求用显式广播(setPackage/setComponent)+ 权限就能更直接地满足,不需要额外一套框架。
话题三:广播与状态管理的分工。 很多项目把"数据刷新"也做成广播,结果形成"广播套广播"的链路,排查时根本看不出谁触发了谁。更清晰的做法是:状态变化走数据层(Flow/回调),广播只表达"系统层面的事件"。判断标准很简单——如果这个事件的所有处理方都在同一进程内、且能通过某个共享对象拿到数据,那它就不该是广播。
还有一个实践细节值得单说:onReceive 与 onReceiveWithResult 的区别。 后者能返回 setResultCode / setResultData,并且可以主动调 goAsync() 延长处理窗口;前者只是单向通知。同理,LocalBroadcastManager 版与 ContextCompat 版在行为上也已不同。新代码统一用 ContextCompat.registerReceiver + ContextCompat.sendBroadcast,避免 API 级别的行为差异。
三条高频追问
问:Android 8 之后哪些隐式广播还能收?
结论是几乎不能收,除非在清单里显式声明该 action。被限制的是"隐式广播"——即只声明 action 不指定接收组件的那类。系统给出的可注册白名单主要是这几类:启动完成(注意 8.0 起不再包含)、锁屏相关(部分)、输入法相关、ACTION_MY_PACKAGE_REPLACED、以及"以 ACTION_ 前缀结尾的系统广播"这个兜底规则。实际开发里能依赖的极少,所以主流做法是改用 JobScheduler/WorkManager 的约束条件替代广播。
问:广播的优先级 priority 有什么用?
它只影响同一进程内接收器的处理顺序(数值越大越先),跨进程比较的是 Intent 的 priority 属性,且只对有序广播(orderedBroadcast)有意义。它不能用来"抢先"别人的广播,也不能保证及时收到——所以不要用高优先级广播做"紧急通知"这类需求。
问:广播能携带大对象吗?
不能。广播走 Binder 事务,同样有约 1MB 限制。传大对象的正确做法是传 ID,让接收方自己查——这条判据与 Activity 传值、ContentProvider 返回值是同一个(都受 Binder 事务限制),说出来能体现对底层机制的一致理解。
问:怎么用广播做跨进程通信?
现代方案是 Messenger(基于 Handler 的消息封装)或 AIDL,但如果是"通知某个状态变了",优先考虑 ContentProvider 的 ContentObserver 或直接用 回调 + Binder。广播只适合"一对多的状态通知",不适合"一对一的请求-响应"。
落到项目里怎么做
一条能直接落地的规则:通知类需求优先用 Flow/LiveData + 进程内共享,跨进程才用广播,跨场景且需要保证完成用 WorkManager。 广播的定位应该收缩到"系统事件通知"这一类。
配套实践三条:① 自定义 action 统一前缀(com.demo.action.X)并集中成常量,配上自定义权限;② 所有动态注册统一走 ContextCompat.registerReceiver 并在配对的 onStop 注销;③ 用 Lint 拦截过时的隐式广播(系统已废弃的那些),在编译期而不是运行期暴露。
给正在准备面试的你
这题的层次差在"知不知道限制"。推荐这条答题线:先讲静态/动态两种注册与投递路径 → 再讲 onReceive 在主线程、goAsync 的用法与约束 → 然后主动点出 Android 8 隐式广播限制、9 之后后台收紧(主动讲限制是最高分的动作)→ 落到替代方案(JobScheduler/WorkManager 约束、显式广播、进程内 Flow)→ 最后补 Binder 1MB 限制与大对象传 ID 的判据。能把"限制"和"替代方案"连起来讲,答案就有层次了。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:WorkManager:后台任务的最终归宿
下一篇预告:ContentProvider:跨进程数据共享的标准入口
有任何问题欢迎在评论区留言交流。