Android 开发做久了,BroadcastReceiver 这玩意儿你一定绕不开。它算是四大组件里最“轻”的一个,没有界面、生命周期极短,但偏偏在系统事件分发、跨页面通信、模块解耦这些场景里出镜率极高。我见过不少项目,Activity 之间传个数据都要靠广播绕一圈,也见过有人把广播用成了 EventBus,结果代码越写越乱。问题往往不在广播本身,而在于没搞清楚它到底有几种形态、各自适合什么场景、坑埋在哪里。
这篇就把 BroadcastReceiver 的几种典型用法一次讲透:普通广播、有序广播、拦截广播、本地广播、Sticky 广播,以及系统广播。每一种我都会说清楚它的行为特征、适用场景、代码怎么写,以及我在实际项目里踩过的坑。不管你是刚接触 Android 的新手,还是写了几年但一直没系统梳理过广播的老手,看完应该都能有点收获。尤其是本地广播和 Sticky 广播这两块,很多人只知道有这么个东西,真到用的时候才发现细节全是坑。
1. 先把广播的两种注册方式理清楚
在聊具体类型之前,得先把注册方式说清楚,因为后面所有广播的行为都跟注册方式强相关。Android 里注册广播只有两条路:静态注册和动态注册。这两者的差异不只是“写在 Manifest 里还是写在代码里”这么简单,它直接决定了广播能不能收到、什么时候能收到、以及在新版本系统上会不会被限制。
1.1 静态注册:写在 Manifest 里的常驻监听
静态注册就是在AndroidManifest.xml里通过<receiver>标签声明。这种注册方式的特点是:应用没启动也能被系统拉起来接收广播(前提是广播能匹配上)。早期很多应用靠这个做开机自启、后台唤醒,后来系统版本一路收紧,静态注册能收到的隐式广播越来越少。
<receiver android:name=".MyStaticReceiver" android:exported="true"> <intent-filter> <action android:name="com.example.MY_ACTION" /> </intent-filter> </receiver>这里有个必须注意的点:从 Android 8.0(API 26)开始,绝大多数隐式广播不再允许静态注册接收。也就是说,你写了个静态接收器监听某个自定义 action,如果发送方用的是隐式 Intent,那在 8.0 以上的设备上大概率收不到。系统这么做的目的是省电、防止应用被随意唤醒。所以现在静态注册基本只用于接收少数系统白名单广播,比如开机完成、时区变化这类。
提示:
android:exported属性在 Android 12 之后是强制要求显式声明的,不写会直接编译报错。如果这个接收器只接收应用内部广播,务必设为false,避免被外部应用触发。
1.2 动态注册:跟着组件生命周期走
动态注册是在代码里调用registerReceiver(),通常在 Activity 或 Service 的onCreate/onResume里注册,在对应的onDestroy/onPause里注销。它的特点是灵活、可控,而且不受 8.0 隐式广播限制的影响——动态注册的接收器依然能收到大部分隐式广播。
IntentFilter filter = new IntentFilter(); filter.addAction("com.example.MY_ACTION"); myReceiver = new MyReceiver(); registerReceiver(myReceiver, filter);动态注册最大的坑就是忘记注销。注册了不注销,轻则内存泄漏,重则onReceive在组件已经销毁后还被调用,直接崩溃。我个人的习惯是:注册和注销成对写,注册写在onCreate,注销写在onDestroy,中间不要穿插条件判断,避免某条分支漏掉注销。
| 对比项 | 静态注册 | 动态注册 |
|---|---|---|
| 注册位置 | Manifest 文件 | 代码中 |
| 生命周期 | 随应用进程 | 随注册组件 |
| 8.0 隐式广播限制 | 受限 | 不受限 |
| 是否需手动注销 | 否 | 是 |
| 典型场景 | 开机、时区等系统事件 | 页面内、Service 内通信 |
理解了这两种注册方式,后面的内容就好展开了。因为广播的类型差异,本质上是在“发送方式”和“接收范围”上做文章。
2. 普通广播:最基础也最容易被滥用的那一种
普通广播(Normal Broadcast)就是最常规的sendBroadcast()。它的行为特征很明确:所有匹配的接收器都会收到,接收顺序不确定,接收器之间互不影响,一个接收器出问题不影响其他接收器。听起来很美好,但恰恰因为“谁都能收、谁都能发”,它成了最容易被滥用的通信方式。
2.1 普通广播的发送与接收链路
发送一个普通广播非常简单:
Intent intent = new Intent("com.example.NORMAL_ACTION"); intent.putExtra("data", "hello"); sendBroadcast(intent);接收端只要 action 匹配就能收到:
public class MyReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String data = intent.getStringExtra("data"); // 处理数据 } }这里有个关键点很多人忽略:onReceive运行在主线程。这意味着你不能在里面做耗时操作,超过 10 秒会 ANR。如果确实需要处理耗时逻辑,正确做法是在onReceive里启动一个 Service 或者用goAsync()配合线程池处理。我见过有人在onReceive里直接查数据库、发网络请求,结果线上 ANR 率飙升,排查半天才发现是广播惹的祸。
2.2 普通广播的适用边界与滥用后果
普通广播适合什么场景?我的判断标准是:发送方和接收方完全解耦、接收方数量不确定、且对接收顺序没有要求。比如某个数据同步完成后通知多个模块刷新,这种场景用普通广播是合理的。
但很多项目把它用成了“万能通信总线”:Activity 给 Fragment 传数据用广播、点击事件用广播、甚至按钮状态更新也用广播。结果就是代码里到处是sendBroadcast,接收器满天飞,改一个 action 要全局搜索,维护成本极高。更麻烦的是,普通广播是全局的,任何应用只要知道 action 就能监听(动态注册的情况下),存在数据泄露风险。
注意:如果广播携带敏感数据,普通广播的全局可见性就是个隐患。这种情况下应该优先考虑本地广播,或者给广播加上权限校验。
我的经验是:应用内部的通信,能用接口回调、ViewModel、LiveData 解决的,就别用广播。广播留给那些真正需要跨组件、跨进程、或者跟系统事件打交道的场景。
3. 有序广播:能改数据、能拦截的“串行分发”
有序广播(Ordered Broadcast)跟普通广播最大的区别在于:接收器是按优先级顺序逐个接收的,而且前面的接收器可以修改数据、甚至可以完全拦截广播,让后面的接收器收不到。这个特性决定了它的使用场景跟普通广播完全不同。
3.1 优先级机制与数据传递
发送有序广播用sendOrderedBroadcast():
Intent intent = new Intent("com.example.ORDERED_ACTION"); sendOrderedBroadcast(intent, null);接收器的优先级通过IntentFilter的setPriority()设置,数值越大优先级越高:
IntentFilter filter = new IntentFilter("com.example.ORDERED_ACTION"); filter.setPriority(100); registerReceiver(highPriorityReceiver, filter);高优先级的接收器先收到广播,它可以通过setResultData()、setResultExtras()修改结果数据,后面的接收器通过getResultData()、getResultExtras()拿到修改后的数据。这就形成了一条数据处理链:A 处理完传给 B,B 处理完传给 C。
这里有个细节:动态注册的接收器优先级高于静态注册的,同等优先级下动态注册的先收到。这个规则在排查“为什么我的接收器没先收到”时很有用。
3.2 拦截广播:abortBroadcast 的正确用法
拦截广播靠的是abortBroadcast():
@Override public void onReceive(Context context, Intent intent) { if (shouldBlock) { abortBroadcast(); return; } // 正常处理 }调用abortBroadcast()后,优先级比当前接收器低的接收器就收不到这条广播了。这个机制在系统里有实际应用,比如短信拦截类应用就是靠这个实现的(当然现在系统权限收紧了,普通应用很难再这么干)。
但这里有个大坑:abortBroadcast()只对有序广播有效。如果你对一个普通广播调用它,会直接抛RuntimeException。我当年就犯过这个错,把一个普通广播的接收器改成了有序广播的发送方式,结果忘了改abortBroadcast的调用条件,线上直接崩。所以用拦截功能前,一定要确认发送方用的是sendOrderedBroadcast。
| 特性 | 普通广播 | 有序广播 |
|---|---|---|
| 发送方法 | sendBroadcast | sendOrderedBroadcast |
| 接收顺序 | 不确定 | 按优先级 |
| 能否修改数据 | 否 | 能 |
| 能否拦截 | 否 | 能(abortBroadcast) |
| 适用场景 | 通知类、无顺序要求 | 数据处理链、拦截 |
有序广播的性能比普通广播差一些,因为要串行分发。所以如果只是单纯通知,没必要用有序广播,普通广播就够了。
4. 本地广播:应用内通信的安全选择
本地广播(Local Broadcast)是 Android Support 库(后来并入 AndroidX)提供的一个工具,专门解决普通广播的两个痛点:全局可见导致的安全问题,以及跨应用广播带来的性能开销。它的核心特点是:广播只在应用内部传递,不会离开当前应用的进程。
4.1 LocalBroadcastManager 的工作机制
使用本地广播需要先拿到LocalBroadcastManager实例:
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context); // 注册 IntentFilter filter = new IntentFilter("com.example.LOCAL_ACTION"); manager.registerReceiver(localReceiver, filter); // 发送 Intent intent = new Intent("com.example.LOCAL_ACTION"); manager.sendBroadcast(intent); // 注销 manager.unregisterReceiver(localReceiver);它的内部实现其实不是真正的广播机制,而是基于 Handler 的消息传递。发送方把 Intent 封装成消息投递到主线程的 Handler,接收器在主线程回调。所以它本质上是一个“披着广播外衣的进程内事件总线”。
这个实现方式带来几个好处:第一,广播不出应用,其他应用无法监听,安全性高;第二,不需要跨进程通信(IPC),性能比普通广播好;第三,不受系统对隐式广播的限制影响。
4.2 本地广播的局限与替代方案
但本地广播也不是没有缺点。首先,它已经被标记为废弃(deprecated),官方推荐用LiveData或者响应式流来替代进程内通信。其次,它只能在应用内使用,跨进程场景完全用不了。再者,它的 API 设计跟系统广播类似,容易让人误以为可以跨应用,造成理解混乱。
我在实际项目里的做法是:新项目直接用LiveData或者Flow做进程内通信,不再引入LocalBroadcastManager。如果是维护老项目,遇到已有的本地广播代码,能不动就不动,但新写的模块不会再往里加。如果非要用广播做进程内通信,本地广播确实比普通广播安全,但要注意它的废弃状态,别在新技术选型里再选它。
提示:本地广播的
onReceive同样运行在主线程,耗时操作一样要避免。这一点跟普通广播没有区别。
5. Sticky 广播:发送后注册也能收到的“粘性”机制
Sticky 广播是个挺特殊的存在。普通广播是“发送时注册了才能收到”,而 Sticky 广播是“发送后,即使接收器还没注册,等它注册时依然能收到之前发送的那条广播”。这个特性听起来很实用,但实际用起来坑非常多,而且在新版本系统上已经被废弃。
5.1 Sticky 广播的发送与获取
发送 Sticky 广播用sendStickyBroadcast():
Intent intent = new Intent("com.example.STICKY_ACTION"); intent.putExtra("battery", 88); sendStickyBroadcast(intent);接收端注册后,系统会立即把最后一条 Sticky 广播回调给它:
IntentFilter filter = new IntentFilter("com.example.STICKY_ACTION"); registerReceiver(stickyReceiver, filter);也可以主动去“捞”最后一条 Sticky 广播:
Intent stickyIntent = registerReceiver(null, filter); if (stickyIntent != null) { int battery = stickyIntent.getIntExtra("battery", -1); }传null作为接收器,就是只查询不注册,这个用法在获取电量、充电状态时比较常见。
5.2 为什么 Sticky 广播被废弃了
Sticky 广播被废弃的原因主要有几个。第一,它需要SEND_STICKY_BROADCAST权限,而这个权限是系统级的,普通应用根本申请不到,所以实际上只有系统应用能用。第二,Sticky 广播会一直保留在系统里,占用内存,而且数据可能过期,接收方拿到的是“历史数据”而不是“当前状态”,容易造成逻辑错误。第三,它的行为不够直观,很多开发者搞不清楚什么时候能收到、什么时候收不到,调试成本高。
现在获取系统状态(比如电量、网络状态)都有更规范的 API,比如BatteryManager、ConnectivityManager,不需要再依赖 Sticky 广播。所以我的建议很直接:新项目不要用 Sticky 广播,老项目里如果遇到了,评估一下能不能换成对应的系统 API。
| 特性 | 普通广播 | Sticky 广播 |
|---|---|---|
| 注册前发送能否收到 | 否 | 能 |
| 是否需要特殊权限 | 否 | 是(系统级) |
| 数据时效性 | 实时 | 可能是历史数据 |
| 当前状态 | 正常使用 | 已废弃 |
6. 系统广播:跟系统事件打交道的正确姿势
系统广播是 Android 系统在特定事件发生时发出的广播,比如开机完成、网络变化、电量变化、屏幕开关等。开发者通过监听这些广播,可以让应用对系统状态做出响应。但系统广播这块的坑,比前面几种加起来都多,主要原因是系统版本一直在收紧限制。
6.1 常见系统广播与版本适配
先列几个常用的系统广播 action:
| 广播 Action | 触发时机 | 注册方式建议 |
|---|---|---|
| ACTION_BOOT_COMPLETED | 开机完成 | 静态注册 |
| ACTION_TIMEZONE_CHANGED | 时区变化 | 静态注册 |
| ACTION_CONNECTIVITY_CHANGE | 网络变化 | 动态注册 |
| ACTION_BATTERY_CHANGED | 电量变化 | 动态注册(Sticky) |
| ACTION_SCREEN_ON/OFF | 屏幕开关 | 动态注册 |
这里的关键适配点是 Android 8.0 的隐式广播限制。除了少数白名单广播(如开机、时区),大部分系统广播不再允许静态注册接收。所以像网络变化、电量变化这类,必须改成动态注册。如果你的应用依赖静态注册监听网络变化,在 8.0 以上设备上会完全失效,这个坑我踩过,排查了半天才发现是版本限制。
6.2 系统广播监听的性能与合规考量
监听系统广播不是越多越好。每多监听一个广播,应用被唤醒的机会就多一次,耗电也就上去了。尤其是静态注册的广播,应用进程可能因为一条广播被拉起,用户感知就是“这应用怎么老在后台跑”。所以我的原则是:只监听真正需要的系统广播,能用其他 API 替代的就替代。
比如网络状态,与其监听ACTION_CONNECTIVITY_CHANGE,不如用ConnectivityManager.registerDefaultNetworkCallback(),更精准也更省电。再比如电量,BatteryManager提供的 API 比监听广播更直接。系统广播适合那些“没有更好替代方案”的场景,比如开机自启、时区变化这种。
注意:从 Android 10 开始,后台启动 Activity 受到严格限制,很多依赖系统广播拉起界面的做法已经行不通了。设计功能时要把这个限制考虑进去,别做出一个在新系统上根本跑不起来的方案。
7. 几种广播的选型对照与实战避坑清单
聊完这几种广播,最后做个选型对照,再把我这些年踩过的坑集中列一下。选型这件事没有绝对的对错,关键是匹配场景。
7.1 一张表搞定广播选型
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 应用内模块解耦通信 | LiveData / Flow | 比广播更轻、更安全 |
| 应用内必须用广播 | 本地广播 | 不出应用,安全 |
| 通知多个模块,无顺序要求 | 普通广播 | 简单直接 |
| 需要按顺序处理、可拦截 | 有序广播 | 支持优先级和 abort |
| 获取系统最后状态 | 对应系统 API | Sticky 已废弃 |
| 监听系统事件 | 动态注册 + 白名单静态 | 适配 8.0 限制 |
选型的核心逻辑就一句话:能用更轻量的方案就别用广播,非要用广播就选范围最小的那种。本地广播比普通广播范围小,普通广播比全局隐式广播范围小,层层收窄,安全性和性能都更好。
7.2 我踩过的那些广播坑
第一个坑:onReceive里做耗时操作导致 ANR。这个前面提过,但真的太多人中招。记住onReceive在主线程,超过 10 秒就 ANR,耗时逻辑要么丢给 Service,要么用goAsync()。
第二个坑:动态注册忘记注销导致内存泄漏。尤其是把接收器注册在 Activity 里,Activity 销毁了接收器还活着,持有 Activity 引用,泄漏就这么来的。成对写注册注销,别偷懒。
第三个坑:对普通广播调用abortBroadcast()导致崩溃。这个错误很隐蔽,因为编译期不报错,运行到那行才崩。用拦截功能前确认发送方是有序广播。
第四个坑:静态注册监听隐式广播在 8.0 以上失效。这个坑的迷惑性在于,低版本设备上一切正常,高版本设备上静默失效,没有任何报错。适配时一定要在目标版本上实测。
第五个坑:Sticky 广播数据过期导致逻辑错误。接收方拿到的是历史数据,如果业务逻辑假设这是当前状态,就会出问题。能用系统 API 就别用 Sticky。
第六个坑:广播携带敏感数据被外部应用监听。普通广播是全局的,动态注册的接收器任何应用都能注册监听。敏感数据要么用本地广播,要么加密,要么加权限校验。
7.3 关于 goAsync 的一个实用技巧
最后分享一个goAsync()的用法,这个在需要异步处理广播时很有用:
@Override public void onReceive(Context context, Intent intent) { final PendingResult pendingResult = goAsync(); new Thread(() -> { try { // 耗时操作 doSomethingSlow(); } finally { pendingResult.finish(); } }).start(); }goAsync()会返回一个PendingResult,告诉系统“我还在处理,别回收我”。处理完调用finish()释放。这样既避免了 ANR,又不会因为onReceive返回后广播被回收导致处理中断。注意finish()一定要在finally里调用,否则异常路径下会泄漏。
广播这东西,说简单也简单,说坑多也是真多。我的整体态度是:把它当成一个“系统级的事件通知机制”来用,而不是“应用内的通信总线”。应用内通信有太多比广播更合适的方案,广播的价值在于跟系统打交道、以及跨进程这种特殊场景。把范围收窄,把注册注销管好,把主线程意识刻在脑子里,广播用起来就不会出大问题。