1. 广播机制到底解决了什么问题
做过Android开发的人都有一个体会:Activity和Service之间的通信,如果全靠显式Intent和Binder,代码很快就会变成一团乱麻。尤其是当多个模块需要同时响应同一个事件时——比如网络状态变了、电量低了、屏幕熄灭了——你不可能让每个模块都去轮询系统状态,那样既费电又低效。BroadcastReceiver这套广播机制,本质上就是Android给出的一对多事件分发方案:一个发送方发出消息,多个接收方按需订阅、各自处理,彼此之间不需要知道对方的存在。
这个标题里一口气列了六种广播类型——普通广播、有序广播、拦截广播、本地广播、Sticky广播、系统广播。很多人刚接触的时候会觉得概念太多、容易混淆,但实际用起来你会发现,它们之间的区别无非就是三个维度:发送顺序是否可控、能否中途拦截、作用范围是全局还是应用内、以及发送后是否保留给后注册的接收者。把这四个维度搞清楚,六种类型自然就分清了。
这篇文章适合谁看?如果你已经写过几个Activity跳转、对Intent有基本概念,但在广播这块总是似懂非懂——比如不知道为什么Android 8.0之后静态注册收不到隐式广播了、不知道为什么本地广播比全局广播更安全、不知道为什么Sticky广播被废弃了——那这篇内容就是给你准备的。我会从实际项目角度出发,把每种广播的用法、适用场景、踩坑点都拆开讲清楚,代码可以直接抄作业。
2. 六种广播类型逐一拆解
2.1 普通广播:最基础的一对多分发
普通广播(Normal Broadcast)是最常用的一种。通过sendBroadcast(intent)发送,所有注册了对应action的接收者都会收到,接收顺序不确定,而且接收者之间互不干扰。你可以把它想象成小区里用大喇叭喊了一嗓子“停水了”,谁听见谁去接水,喊话的人不关心谁听到了、谁先到的。
普通广播的核心特点有三个:第一,发送后所有匹配的接收者几乎同时收到(实际上是主线程消息队列里排队处理);第二,接收者不能修改Intent中的数据传递给下一个接收者;第三,发送方无法知道谁接收了、处理结果如何。
代码上非常简单,发送端:
Intent intent = new Intent("com.example.ACTION_UPDATE"); intent.putExtra("data", "new_value"); sendBroadcast(intent);接收端动态注册:
IntentFilter filter = new IntentFilter("com.example.ACTION_UPDATE"); BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String data = intent.getStringExtra("data"); // 处理逻辑 } }; registerReceiver(receiver, filter);这里有个新手最容易踩的坑:onReceive方法运行在主线程,默认执行时间不能超过10秒,否则会ANR。如果你在onReceive里做网络请求或者数据库大批量写入,必崩。正确做法是在onReceive里启动一个IntentService或者用WorkManager把耗时操作丢到后台去。
注意:动态注册的接收者必须在合适的生命周期回调里调用
unregisterReceiver(),否则会造成内存泄漏。通常建议在onResume注册、onPause注销,或者直接在onDestroy里注销。
2.2 有序广播:按优先级串行传递
有序广播(Ordered Broadcast)通过sendOrderedBroadcast(intent, receiverPermission)发送。和普通广播最大的区别是:接收者按优先级顺序一个一个接收,优先级高的先收到,处理完后可以决定是否继续传递给下一个。
优先级的设置方式是在IntentFilter里用setPriority(),数值越大优先级越高,范围是-1000到1000。比如:
IntentFilter filter = new IntentFilter("com.example.ACTION_ORDERED"); filter.setPriority(100); // 高优先级 registerReceiver(highPriorityReceiver, filter);有序广播最典型的应用场景是短信拦截类的功能——高优先级的接收者先拿到消息,判断如果是垃圾信息就直接abortBroadcast()终止传递,后面的接收者就收不到了。这也是“拦截广播”这个说法的来源。
有序广播还有一个能力是修改数据向下传递。高优先级的接收者可以通过setResultData()或者setResultExtras()修改结果,后面的接收者拿到的是修改后的数据。这个特性在一些需要逐级处理的场景里很有用,比如一个订单状态变更事件,先经过风控模块过滤,再经过日志模块记录,最后到UI模块刷新。
但有序广播也有代价:因为是串行的,如果接收者多、处理慢,整体耗时会明显增加。而且任何一个接收者超时(超过10秒)都会导致整个广播链出问题。所以有序广播不适合高频事件,比如传感器数据更新就绝对不能用有序广播。
2.3 拦截广播:有序广播的终止能力
严格来说,“拦截广播”并不是一种独立的广播类型,而是有序广播的一个能力——通过abortBroadcast()方法终止广播继续传递。但因为它太常用了,很多资料会把它单独拿出来讲。
拦截的逻辑是这样的:高优先级接收者收到广播后,如果判断这个事件不需要继续传播,调用abortBroadcast(),系统就会停止将广播分发给后续接收者。注意,只有有序广播才能拦截,普通广播调用abortBroadcast()会抛异常。
@Override public void onReceive(Context context, Intent intent) { if (shouldBlock(intent)) { abortBroadcast(); // 终止传递 setResultData("blocked"); } }实际项目中,拦截广播常用于:权限校验(低权限模块直接拦截敏感事件)、去重(同一个事件只让第一个接收者处理)、条件过滤(不满足条件的事件不向下传)。
这里有个容易忽略的细节:abortBroadcast()只是阻止后续接收者收到广播,但当前接收者自己的onReceive还是会执行完。而且如果多个高优先级接收者都调用了abortBroadcast(),只有第一个生效,后面的根本收不到广播,自然也没机会调用。
提示:有序广播的优先级只在动态注册时有效,静态注册的接收者在Android 8.0之后对隐式广播的限制下基本收不到有序广播,这一点后面会详细说。
2.4 本地广播:应用内的安全通道
本地广播(Local Broadcast)是Android Support库(后来并入AndroidX)提供的一个工具,核心类是LocalBroadcastManager。它解决的问题很明确:普通广播是全局的,任何应用只要知道action就能注册接收,存在安全和性能隐患。
普通广播的隐患有两个:第一,你发送的Intent可能被其他应用接收,造成数据泄露;第二,其他应用可以向你发送伪造的广播,造成安全风险。本地广播把广播范围限制在当前应用进程内,其他应用完全无法参与。
使用方式:
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context); // 注册 manager.registerReceiver(receiver, filter); // 发送 manager.sendBroadcast(intent); // 注销 manager.unregisterReceiver(receiver);本地广播的性能也比全局广播好,因为它不走系统AMS(ActivityManagerService)的跨进程通信,直接在应用进程内部分发,开销小很多。
但要注意,LocalBroadcastManager在AndroidX中已经被标记为废弃。官方推荐的替代方案是LiveData或者Flow这类响应式组件。不过在很多老项目中,本地广播仍然大量存在,维护老代码时你必须能看懂它。而且对于简单的模块间通信,本地广播的API确实比LiveData更直观,所以新项目里也不是完全不能用,只是要知道它不是长期方向。
2.5 Sticky广播:发送后保留,后注册也能收到
Sticky广播(粘性广播)的特点是:广播发送后不会立刻消失,而是保留在系统里,之后注册的接收者也能收到最近一次发送的广播。通过sendStickyBroadcast(intent)发送,注册时用registerReceiver(receiver, filter)的返回值就能拿到之前发送的Intent。
典型场景是状态类广播:比如电量变化、网络状态变化。你希望新启动的组件能立刻知道当前状态,而不是等下一次状态变化才收到通知。Sticky广播就是为这个设计的。
// 发送粘性广播 sendStickyBroadcast(intent); // 注册时获取最后一次的粘性广播 Intent stickyIntent = registerReceiver(receiver, filter); if (stickyIntent != null) { // 处理历史状态 }但Sticky广播在Android 5.0(API 21)之后被废弃,原因是安全和性能问题:粘性广播会一直保留在系统里,占用内存;而且任何应用都能读取粘性广播的内容,存在信息泄露风险。现在如果你需要类似能力,应该用LiveData的observe配合getValue(),或者自己维护一个状态缓存。
维护老项目时如果遇到Sticky广播,要知道它的行为:removeStickyBroadcast(intent)可以移除,但实际项目中很少这么用。更常见的做法是直接迁移到状态管理组件。
2.6 系统广播:系统事件的标准通知
系统广播是Android系统在特定事件发生时自动发出的广播,比如开机完成(ACTION_BOOT_COMPLETED)、网络变化(CONNECTIVITY_CHANGE)、电量低(ACTION_BATTERY_LOW)、屏幕开关(ACTION_SCREEN_ON/ACTION_SCREEN_OFF)等。开发者只需要注册对应的接收者就能监听这些事件。
系统广播的action都是系统预定义的常量,比如:
IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_BOOT_COMPLETED); filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); filter.addAction(Intent.ACTION_BATTERY_LOW);但这里有一个Android 8.0(API 26)之后的重大变化:为了省电和减少后台唤醒,系统对静态注册的隐式广播做了严格限制。大部分系统广播不能再通过静态注册(在AndroidManifest.xml里声明)接收,必须动态注册。只有少数例外,比如ACTION_BOOT_COMPLETED、ACTION_LOCALE_CHANGED等白名单内的广播仍然可以静态注册。
这个变化导致很多老代码在Android 8.0+上失效——比如以前在Manifest里注册CONNECTIVITY_ACTION来监听网络变化,现在必须改成动态注册,或者用ConnectivityManager.registerDefaultNetworkCallback()这种更现代的方式。
注意:动态注册的系统广播接收者,一定要在合适的时机注销。尤其是监听网络、电量这类高频广播,如果忘记注销,会持续消耗电量。
3. 注册方式与版本适配实战
3.1 静态注册与动态注册的本质区别
静态注册是在AndroidManifest.xml里声明接收者,应用安装时系统就会注册,即使应用没有运行也能收到广播。动态注册是在代码里调用registerReceiver(),只在注册期间有效,应用进程被杀后就失效。
静态注册的写法:
<receiver android:name=".MyReceiver" android:exported="false"> <intent-filter> <action android:name="com.example.ACTION_CUSTOM" /> </intent-filter> </receiver>动态注册的写法前面已经展示过。两者的选择逻辑很简单:需要应用未启动时也能响应的事件,用静态注册;只需要应用运行期间响应的事件,用动态注册。但Android 8.0之后,静态注册能用的场景被大幅压缩,大部分情况下你只能用动态注册。
android:exported属性也需要注意:如果接收者需要接收其他应用的广播,必须设为true;如果只接收应用内部的广播,设为false更安全。Android 12(API 31)之后,如果声明了intent-filter,exported必须显式指定,否则编译报错。
3.2 Android 8.0之后的隐式广播限制
这是广播这块最大的版本适配坑。Android 8.0开始,针对API 26及以上的应用,大部分隐式广播(即没有明确指定目标组件的广播)不能再静态注册。系统这样做的目的是减少后台唤醒、延长续航。
受影响的广播包括:CONNECTIVITY_ACTION、ACTION_NEW_OUTGOING_CALL、ACTION_SCREEN_ON等。不受影响的(白名单)包括:ACTION_BOOT_COMPLETED、ACTION_LOCALE_CHANGED、ACTION_USB_ACCESSORY_ATTACHED等。
适配方案有三种:
- 改成动态注册:在Activity或Service的合适生命周期里注册,用完注销。这是最直接的方式。
- 用JobScheduler或WorkManager替代:比如网络状态监听,可以用
ConnectivityManager.registerDefaultNetworkCallback()。 - 用显式广播:如果广播是发给自己的,直接指定包名
intent.setPackage(getPackageName()),这样就是显式广播,不受限制。
// 显式广播,不受隐式广播限制 Intent intent = new Intent("com.example.ACTION_INTERNAL"); intent.setPackage(getPackageName()); sendBroadcast(intent);3.3 前台广播与后台广播的性能差异
从Android 8.0开始,广播还分前台和后台。前台广播通过sendBroadcast(intent, null)发送(或者带权限参数),后台广播通过sendBroadcast(intent)发送。区别在于超时时间:前台广播的接收者超时是10秒,后台广播是60秒。
但更重要的是,后台广播在应用处于后台时可能被延迟处理,甚至被系统丢弃。如果你的广播需要及时响应,应该用前台广播,或者考虑用startForegroundService配合其他机制。
实际项目中,我一般建议:应用内部的模块通信优先用本地广播或LiveData,跨应用通信才用全局广播,系统事件监听用动态注册的系统广播。这样能避开大部分版本适配问题。
4. 常见问题与排查技巧实录
4.1 广播收不到的排查思路
广播收不到是最常见的问题,排查时按以下顺序检查:
| 排查项 | 检查内容 | 常见原因 |
|---|---|---|
| action匹配 | 发送和注册的action字符串是否完全一致 | 大小写、拼写错误 |
| 注册方式 | 静态还是动态,是否在有效期内 | 动态注册已注销 |
| 版本限制 | 是否受Android 8.0隐式广播限制 | 静态注册了受限广播 |
| 权限 | 发送方和接收方权限是否匹配 | 权限声明缺失 |
| 进程状态 | 应用进程是否存活 | 进程被杀,动态注册失效 |
| exported | 组件是否允许外部访问 | exported=false但跨应用发送 |
我踩过最坑的一次是:action字符串在发送端写的是com.example.UPDATE,接收端写的是com.example.update,大小写不一致,排查了半天。所以action字符串一定要用常量定义,不要手写。
4.2 onReceive执行超时与ANR
onReceive默认在主线程执行,超过10秒(前台广播)或60秒(后台广播)就会ANR。很多人以为在onReceive里new Thread()就没事了,但**onReceive返回后系统可能回收广播接收者对象**,导致子线程里的逻辑出问题。
正确做法是:在onReceive里启动一个IntentService(已废弃但老项目常见)或者用WorkManager、JobIntentService把任务交给系统调度。如果只是简单逻辑,直接在onReceive里处理完即可。
@Override public void onReceive(Context context, Intent intent) { // 不要在这里做耗时操作 Intent serviceIntent = new Intent(context, MyIntentService.class); serviceIntent.putExtra("data", intent.getStringExtra("data")); context.startService(serviceIntent); }4.3 有序广播的优先级陷阱
有序广播的优先级只在动态注册时有效,而且相同优先级的接收者顺序不确定。另外,如果高优先级接收者调用了abortBroadcast(),低优先级的接收者完全收不到广播,连日志都不会打。调试时如果发现某个接收者没反应,先检查是不是被前面的接收者拦截了。
还有一个细节:setResultData()设置的数据,只有后续接收者能通过getResultData()拿到,发送方拿不到。如果发送方需要知道处理结果,得用sendOrderedBroadcast的重载版本传入一个resultReceiver。
4.4 本地广播的废弃与迁移
LocalBroadcastManager废弃后,官方推荐用LiveData。迁移思路是:把广播的action映射成LiveData的key,发送广播变成LiveData.setValue(),注册接收者变成LiveData.observe()。对于复杂的事件传递,可以用Flow配合SharedFlow。
但迁移不是必须的,LocalBroadcastManager目前仍然可用,只是不再更新。如果项目里已经大量使用,没必要为了迁移而迁移,等重构时再处理。
5. 广播选型决策与最佳实践
5.1 六种广播的选型对照表
| 广播类型 | 发送方法 | 接收顺序 | 能否拦截 | 作用范围 | 适用场景 |
|---|---|---|---|---|---|
| 普通广播 | sendBroadcast | 不确定 | 否 | 全局 | 一般事件通知 |
| 有序广播 | sendOrderedBroadcast | 按优先级 | 是 | 全局 | 需要顺序处理或拦截 |
| 拦截广播 | 有序广播+abortBroadcast | 按优先级 | 是 | 全局 | 权限校验、去重 |
| 本地广播 | LocalBroadcastManager | 不确定 | 否 | 应用内 | 模块间通信 |
| Sticky广播 | sendStickyBroadcast | 不确定 | 否 | 全局 | 状态类事件(已废弃) |
| 系统广播 | 系统发送 | 不确定 | 否 | 全局 | 监听系统事件 |
选型逻辑可以总结成一句话:应用内通信用本地广播,需要顺序或拦截用有序广播,监听系统事件用动态注册的系统广播,其他情况用普通广播。Sticky广播除非维护老项目,否则不要用。
5.2 广播使用的三条铁律
第一条:能不用广播就不用广播。广播是跨模块通信的重型武器,如果只是Activity和Fragment之间传数据,用ViewModel或者接口回调就够了。广播的跨进程特性带来灵活性的同时,也带来了性能开销和安全风险。
第二条:动态注册必须配对注销。我见过太多项目在onCreate里注册广播,却忘了在onDestroy里注销,导致内存泄漏和重复接收。建议用LifecycleObserver或者registerReceiver的RECEIVER_NOT_EXPORTED标志来管理生命周期。
第三条:敏感数据不要走全局广播。全局广播的Intent可能被其他应用读取,密码、token这类数据绝对不能通过广播传递。如果必须传,用本地广播或者加密后再传。
5.3 Android 13+的广播新变化
Android 13(API 33)对广播又做了一些调整,主要是动态注册接收者时必须指定exported行为。以前registerReceiver(receiver, filter)就行,现在推荐用registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)明确声明不对外暴露。如果接收的是系统广播,用Context.RECEIVER_EXPORTED。
// Android 13+ 推荐写法 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED); } else { registerReceiver(receiver, filter); }这个变化的核心目的还是安全:让开发者显式声明广播的可见范围,避免无意中暴露内部通信。
6. 一个完整的模块间通信实例
假设我们有一个下载模块和一个UI模块,下载完成后需要通知UI刷新。用本地广播实现:
下载模块发送:
Intent intent = new Intent("com.example.DOWNLOAD_COMPLETE"); intent.putExtra("file_id", fileId); LocalBroadcastManager.getInstance(context).sendBroadcast(intent);UI模块注册:
private BroadcastReceiver downloadReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String fileId = intent.getStringExtra("file_id"); refreshUI(fileId); } }; @Override protected void onStart() { super.onStart(); IntentFilter filter = new IntentFilter("com.example.DOWNLOAD_COMPLETE"); LocalBroadcastManager.getInstance(this).registerReceiver(downloadReceiver, filter); } @Override protected void onStop() { super.onStop(); LocalBroadcastManager.getInstance(this).unregisterReceiver(downloadReceiver); }这个例子里,注册和注销配对放在onStart/onStop,保证UI可见时才接收,避免后台无效刷新。action用常量定义,避免拼写错误。数据通过putExtra传递,简单直接。
如果换成LiveData实现,下载模块持有一个MutableLiveData<String>,UI模块observe它,代码会更简洁,而且没有注册注销的负担。所以新项目我更推荐LiveData或Flow,本地广播只在维护老代码时使用。
7. 我个人在实际项目中的体会
广播这块知识,刚学的时候觉得概念多、容易混,但用久了会发现它的设计其实很清晰:普通广播解决一对多通知,有序广播解决顺序和拦截,本地广播解决安全和性能,系统广播解决系统事件监听。每一种都有明确的适用边界,选错了类型,要么性能差,要么有安全风险,要么在某个Android版本上直接失效。
我踩过最大的坑是Android 8.0的隐式广播限制。当时一个老项目在Android 8.0手机上网络监听完全失效,排查了一整天才发现是静态注册的CONNECTIVITY_ACTION被系统限制了。后来改成动态注册,问题解决。从那以后,我养成了一个习惯:每次涉及广播的代码,先确认目标API级别,再确认注册方式是否受版本限制。
最后分享一个小技巧:调试广播时,可以在onReceive的第一行打日志,把action和extras都打出来。如果日志没打,说明广播根本没到;如果打了但逻辑没执行,说明是逻辑问题。这个简单的习惯能帮你快速定位问题出在广播层还是业务层。