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() Intent AMS 匹配Receiver 匹配结果 进程A Receiver 1 优先级: 100 进程B Receiver 2 优先级: 50 进程C Receiver 3 优先级: 10 有序广播按优先级依次传递,每个Receiver可调用abortBroadcast()终止 普通广播则同时发送给所有匹配的Receiver 发送方 AMS 接收方进程

流程其实不复杂:

  1. 发送方调用sendBroadcast()或sendOrderedBroadcast(),把Intent扔给AMS。
  2. AMS收到后,根据Intent中的action、category等信息,去PKMS(PackageManagerService)查询所有匹配的Receiver。
  3. 如果是普通广播,AMS会同时把广播分发给所有匹配的Receiver(跨进程通过Binder)。
  4. 如果是有序广播,AMS会按优先级排序,依次分发。每个Receiver处理完后,AMS才会继续分发下一个。
  5. 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检测等等。如果你在实际项目中遇到什么奇葩问题,欢迎来公众号找我聊。

公众号:蓝海资料掘金营,微信deep3321