9、BroadcastReceiver与Intent机制:广播注册、发送、接收流程,粘性广播与有序广播
各位同学,今天我们来聊聊Android四大组件里最容易被忽视、但坑最多的一个——BroadcastReceiver。说实话,我见过不少开发做了三五年,还在把广播当全局事件总线瞎用。广播机制背后牵扯到AMS、Intent匹配、进程间通信,甚至还有Android 8.0以后的那些“断舍离”改动。今天我就把这块掰开揉碎了讲清楚。
9.1 广播注册:静态与动态,你选对了吗?
广播注册有两种方式:静态注册和动态注册。很多人觉得静态注册省事,清单文件里配一下就行。但我要说,静态注册在Android 8.0以后已经被严重限制了。
核心区别:
- 静态注册:在AndroidManifest.xml中声明,应用未启动也能接收广播。但8.0之后,大部分隐式广播都不再支持静态注册。
- 动态注册:在代码中调用registerReceiver(),必须等应用启动后才能注册。生命周期跟随组件,记得在onDestroy里unregister。
我曾经在一个项目中,为了监听网络变化做自动重连,用了静态注册。结果在Android 9的设备上死活收不到广播。查了半天才发现,CONNECTIVITY_ACTION从Android 7.0开始就不支持静态注册了。嗯,这个坑我替你们踩过了。
静态注册示例
<receiver android:name=".MyReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
动态注册示例
// 注册
IntentFilter filter = new IntentFilter("com.example.MY_ACTION");
registerReceiver(myReceiver, filter);
// 别忘了取消注册
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(myReceiver);
}
注意:动态注册如果忘了unregister,会引发内存泄漏。我曾经排查过一个线上OOM问题,最后发现就是某个Activity里注册了广播但没取消,导致Receiver和Activity都无法被GC回收。
9.2 广播发送:普通广播、有序广播、粘性广播
广播发送有三种类型,很多人只用了第一种。但实际项目中,后两种往往能解决更复杂的问题。
| 类型 | 特点 | 接收顺序 | 可被拦截 | Android 8.0+ |
|---|---|---|---|---|
| 普通广播 | 异步、无序、一次性 | 无序 | 否 | 正常 |
| 有序广播 | 同步、按优先级顺序传递 | 按android:priority | 是(abortBroadcast) | 正常 |
| 粘性广播 | 发送后保留,新注册者可收到 | 无序 | 否 | 已废弃 |
9.2.1 普通广播
说白了就是“发出去就不管了”。所有匹配的Receiver都会收到,但谁先谁后不确定。我建议你不要依赖普通广播的执行顺序,否则很容易出bug。
Intent intent = new Intent("com.example.MY_ACTION");
intent.putExtra("key", "value");
sendBroadcast(intent);
9.2.2 有序广播
这个就有意思了。广播会按照Receiver的优先级依次传递,而且每个Receiver都可以选择是否继续往下传。我在做消息推送拦截时就用过这个——高优先级的Receiver可以提前处理,甚至终止广播。
// 发送有序广播
sendOrderedBroadcast(intent, null);
// 在Receiver中拦截
@Override
public void onReceive(Context context, Intent intent) {
// 处理逻辑
abortBroadcast(); // 终止广播继续传递
}
我的经验:有序广播的优先级在intent-filter中设置,数值越大优先级越高。但要注意,相同优先级的Receiver,系统会按注册顺序处理。我曾经在系统级开发中,利用有序广播实现了“先拦截、后放行”的权限控制逻辑,效果非常好。
9.2.3 粘性广播
粘性广播在Android 5.0就已经标记为废弃了。为什么?因为它会导致状态不一致和内存泄漏。广播发送后会一直保留在系统中,新注册的Receiver会立即收到最后一条粘性广播。你想想看,如果某个状态已经变了,但新来的Receiver还拿着旧状态,是不是很坑?
避坑指南:我曾经在一个遗留项目中看到粘性广播的使用,结果导致应用重启后收到过期的广播,引发了一系列空指针异常。如果你现在还在用粘性广播,我建议你尽快迁移到LiveData或者EventBus这类现代方案。
9.3 广播接收流程:从发送到onReceive的完整链路
广播从发送到接收,中间经历了什么?我画了一张图,帮你理清整个流程。
流程其实不复杂:
- 发送方调用sendBroadcast()或sendOrderedBroadcast(),把Intent扔给AMS。
- AMS收到后,根据Intent中的action、category等信息,去PKMS(PackageManagerService)查询所有匹配的Receiver。
- 如果是普通广播,AMS会同时把广播分发给所有匹配的Receiver(跨进程通过Binder)。
- 如果是有序广播,AMS会按优先级排序,依次分发。每个Receiver处理完后,AMS才会继续分发下一个。
- Receiver的onReceive()方法在主线程执行,所以不能做耗时操作。我建议你onReceive里不要超过10秒,否则系统会报ANR。
关键点:广播的onReceive()运行在主线程。如果你需要做耗时操作,记得启动Service或者使用goAsync()。goAsync()可以让你在子线程处理,但也要在限定时间内调用finish(),否则还是会ANR。
9.4 广播的底层实现:Binder与Intent匹配
广播的底层通信依赖Binder。AMS和你的应用进程之间,通过Binder驱动传递Intent数据。这里有个细节:Intent中的数据大小有限制,Binder的传输缓冲区默认是1MB。如果你在Intent里塞了大Bitmap或者长字符串,就会触发TransactionTooLargeException。
我记得有一次线上反馈,用户手机重启后应用闪退。查了半天,发现是某个广播的Intent里塞了一个序列化后的对象,大小超过了1MB。嗯,从那以后我定了个规矩:广播Intent里只传简单类型,复杂数据用EventBus或者直接存数据库。
9.5 Android 8.0以后的广播限制
Android 8.0(API 26)对广播做了大改动。说白了就是:大部分隐式广播不再支持静态注册。为什么?因为Google发现很多应用滥用静态广播,导致系统频繁唤醒,耗电严重。
哪些广播还能静态注册?我列个表:
| 广播Action | 是否支持静态注册 | 备注 |
|---|---|---|
| BOOT_COMPLETED | 是 | 开机广播,必须静态注册 |
| LOCKED_BOOT_COMPLETED | 是 | 设备加密后的开机广播 |
| QUICKBOOT_POWERON | 是 | 快速开机广播 |
| 其他隐式广播 | 否 | 必须动态注册 |
避坑指南:如果你在Android 8.0以上设备上静态注册了隐式广播,系统会直接忽略。我曾经在适配Android 10时,发现某个广播收不到,排查了半天才发现是静态注册的问题。解决方案很简单:要么改成动态注册,要么使用显式广播(指定包名和类名)。
9.6 本地广播:一个被遗忘的好东西
说到广播,不得不提LocalBroadcastManager。它是Android Support包提供的本地广播机制,只在应用内部传递,不会跨进程。相比全局广播,它更安全、更高效。
// 注册本地广播
LocalBroadcastManager.getInstance(this)
.registerReceiver(localReceiver, new IntentFilter("com.example.LOCAL_ACTION"));
// 发送本地广播
LocalBroadcastManager.getInstance(this)
.sendBroadcast(new Intent("com.example.LOCAL_ACTION"));
// 取消注册
LocalBroadcastManager.getInstance(this)
.unregisterReceiver(localReceiver);
我个人的习惯是:能用本地广播,就不用全局广播。除非你真的需要跨进程通信,否则本地广播更安全,也不会被其他应用拦截或伪造。
9.7 总结
广播机制看似简单,但底层涉及AMS、Binder、进程调度等核心知识。我建议你记住几点:
- 静态注册在Android 8.0以后受限,能用动态注册就用动态注册。
- 有序广播适合需要拦截或按顺序处理的场景。
- 粘性广播已废弃,别用了。
- onReceive在主线程,别做耗时操作。
- 本地广播比全局广播更安全、更高效。
好了,这一章就到这里。广播这块内容其实还有很多细节,比如广播的权限控制、动态注册的LeakCanary检测等等。如果你在实际项目中遇到什么奇葩问题,欢迎来公众号找我聊。