这篇是安卓基础系列的第23篇,继续聊广播。前几篇我们把Activity、Service、Fragment这些大块头都过了一遍,到了广播这里,好多初学者会卡在一个问题上:广播到底分几种?什么时候用哪种?为什么有时候我在清单文件里注册了Receiver,却死活收不到开机广播?
这期内容不搞虚的,就围绕“广播类别”这件事,把标准广播、有序广播、动态注册、静态注册、本地广播这些概念一次性理清。你看完至少能回答面试官一个连环问:“广播有哪几种?普通广播和有序广播有什么区别?Android 8.0之后静态注册为什么失效了?”顺便也能把项目里广播收不到、优先级不生效这类坑给踩平。
1. 广播机制到底解决了什么问题
1.1 一个让系统组件互相传话的信使
安卓的广播,本质上是应用之间、系统与应用之间的一种消息传递机制。它很像我们上学时教室里的广播喇叭:校长在广播室说一句话,全校各个教室只要把喇叭开着(注册了接收),都能听到。在安卓里,那个“校长”就是发送方,发送方通过Context.sendBroadcast(intent)把一条Intent扔出去;“全校喇叭”就是各个注册了对应Action的BroadcastReceiver。
这套机制解决的最大痛点是:调用方和被调用方不需要知道彼此的存在。举个最常见例子,系统电量低于20%时,Android系统会发送一个ACTION_BATTERY_LOW广播。你的应用并没有被系统“直接调用”,但只要在合适的位置注册了接收器,就能感知到这个事件,然后弹出低电量提醒或者自动保存数据。这种一对多、低耦合的通信方式,是回调接口和直接调用做不到的。
1.2 广播的三个核心角色
要理解广播类别,得先把参与方认清。一次完整的广播流程里有三方角色:
- 发送方:通过
sendBroadcast()、sendOrderedBroadcast()等方法发出Intent的对象。发送方不需要知道谁会接收。 - 接收方:继承
BroadcastReceiver并实现onReceive()方法的组件。接收方通过注册表达“我想听什么指令”。 - 系统(AMS):安卓系统里的ActivityManagerService负责“分发”。发送方把Intent扔给系统,系统根据Intent携带的Action去匹配所有注册的接收器,然后逐个调用它们的
onReceive()。
这三方的分工决定了广播的整个生命周期。特别提醒一件事:onReceive()是跑在主线程的,官方给的时间窗口很短,如果你在里面做网络请求或者大的文件操作,轻则卡顿,重则直接ANR。别把通知栏快捷开关、开机自启这种看似简单的活儿干成连环车祸现场,后面我会专门说这个问题。
这里还有必要做个名词澄清。安卓里的“广播”指的是BroadcastReceiver这套组件机制,它跟网络里的广播域、BLE里的广播包是两码事。很多人搜“广播域”误入安卓开发区,看半天觉得莫名其妙,就是因为没分清语境。做嵌入式或者网络的同学看到“单播、多播、广播的区别”,那是另一套知识体系,别混着学。
2. 按发送方式划分:标准广播与有序广播
这是面试里问得最多的一对概念。按发送方式,广播可以分为标准广播(Normal Broadcast)和有序广播(Ordered Broadcast)。
2.1 标准广播:异步无序,人人有份
标准广播通过sendBroadcast(intent)发送,它的特点是:系统把广播发出去之后,所有匹配到的接收器都会收到这条消息,接收器之间没有顺序关系,也没有办法拦截这条广播让其他人收不到。
打个比方:你往一个微信群里发了条消息“明天团建”,群里每个人都收到了,你没法控制谁先看到谁后看到,也没法让张三发过的消息在李四那里消失。标准广播就是你发出去就完事,完全异步,发送方不会等待接收方处理结果。
标准广播在代码里长这样:
Intent intent = new Intent("com.example.myapp.MY_NORMAL_ACTION"); intent.putExtra("message", "hello broadcast"); sendBroadcast(intent);接收方在onReceive()里拿到Intent,取出Action和数据:
public class NormalReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if ("com.example.myapp.MY_NORMAL_ACTION".equals(action)) { String msg = intent.getStringExtra("message"); Log.d("NormalReceiver", "收到标准广播: " + msg); } } }这个类本身没什么特殊要求,关键是你得让系统知道它的存在,所以还要配注册逻辑。注册方式我在第3部分详细讲,这里先记住:标准广播适合“通知一下大家”的场景,比如数据下载完成、用户登录状态变化,不需要关心接收方处理结果。
2.2 有序广播:串行接收,可拦截可改写
有序广播走的是sendOrderedBroadcast(intent, receiverPermission)这条路,它跟标准广播最大的区别在于:所有接收器按照优先级从高到低排队,一个接一个地收到广播,并且在任意一环都可以终止广播继续传递,也可以往Intent里写入数据传给下一环。
继续用微信群举例:群里发通知,但这次是按职位顺序传话。大领导先看,觉得这条不合适,一个“撤回”操作,后面的人全看不见了;如果领导觉得没问题,还能在通知后面加一句“各部门必须执行”,再传给下一级。这就是有序广播。
发送有序广播的代码:
Intent intent = new Intent("com.example.myapp.MY_ORDERED_ACTION"); intent.putExtra("message", "原始数据"); sendOrderedBroadcast(intent, null);第二个参数receiverPermission是权限字符串,传null表示不限制接收方权限。如果需要限制只有持有某权限的接收器才能收到,可以传一个自定义权限名。
接收器里的两个关键操作:
// 1. 拦截:后续接收器收不到 abortBroadcast(); // 2. 改写数据:给下一个接收器传东西 setResultCode(Activity.RESULT_OK); setResultData("我改过的数据");下一个接收器取数的时候用的就不是intent.getStringExtra()了,而是:
int resultCode = getResultCode(); String resultData = getResultData();优先级怎么定?动态注册通过IntentFilter.setPriority(),静态注册在<intent-filter>里配android:priority属性,数字越大优先级越高,取值范围是-1000到1000。注意:优先级只在有序广播时才有意义,标准广播里你就算配了priority,接收顺序也不保证。这是很多人踩坑的地方,以为设了高优先级就能抢先收到普通广播,实际是白忙一场。
还有个小技巧:sendOrderedBroadcast还有一个带resultReceiver参数的重载,可以指定一个“最终接收器”,等所有接收器跑完后,在最后收尾拿到最终数据:
BroadcastReceiver finalReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String finalData = getResultData(); Log.d("Final", "最终结果: " + finalData); } }; sendOrderedBroadcast(intent, null, finalReceiver, null, Activity.RESULT_OK, null, null);这个在多个接收器协作处理数据的场景非常有用,比如A先校验合法性,B再补全信息,最后由finalReceiver统一入库。
2.3 两者怎么选:先问自己三个问题
项目里遇到一个场景,先别急着写代码,问自己三个问题:
- 这条消息需要让多个接收方处理吗?如果只需要单个组件响应,其实不一定用广播,本地回调或者事件总线可能更轻量。
- 各接收方之间有没有先后依赖?有依赖就上
sendOrderedBroadcast,没依赖就标准广播。 - 接收方会不会有外部应用?如果广播只是应用内部消息,别用全局广播,优先考虑本地广播或者现代替代方案(第4部分说)。
有一种常见的错误想法是:反正广播发送方和接收方都不知道彼此,所以一律用标准广播最省事。实际上,滥用标准广播会让系统做大量匹配分发,尤其在高频场景下(比如播放器进度更新)非常浪费性能。广播不是万能的,它只是四大组件通信方案里的一种。
3. 按注册方式划分:动态注册与静态注册
广播接收器自身不会凭空生效,必须经过“注册”这一步。注册方式分为动态注册和静态注册,这也是面试里必考的点。
3.1 动态注册实战:跟着生命周期走
动态注册的意思是在代码里通过Context.registerReceiver()方法注册接收器,注册之后接收器才生效,取消注册之后就不再接收广播。这块必须配合生命周期使用,标准做法是在onStart()或onResume()里注册,在onStop()或onPause()里注销,因为不在合适时机注销会导致内存泄漏。
先写一个接收器,监听系统每分钟发出的时间广播ACTION_TIME_TICK:
public class MainActivity extends AppCompatActivity { private final BroadcastReceiver timeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (Intent.ACTION_TIME_TICK.equals(intent.getAction())) { Log.d("TimeReceiver", "系统时间变化,当前整分钟刷新"); } } }; @Override protected void onStart() { super.onStart(); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_TIME_TICK); // targetSdk 34 以上必须显式指定接收器是否对外导出 registerReceiver(timeReceiver, filter, Context.RECEIVER_NOT_EXPORTED); } @Override protected void onStop() { super.onStop(); unregisterReceiver(timeReceiver); } }这里有个版本细节很关键:如果你的targetSdkVersion是34(Android 14)及以上,动态注册必须带第三个参数Context.RECEIVER_EXPORTED或Context.RECEIVER_NOT_EXPORTED,否则会抛SecurityException。系统强制你声明这个广播接收器是否对其它应用开放。像我上面监听ACTION_TIME_TICK是纯系统广播,应用自己监听自己使用,用RECEIVER_NOT_EXPORTED就够了。
可能有人问:为什么用RECEIVER_NOT_EXPORTED?这个标志的含义是“只允许同一个应用或者系统UID发送的广播进入”,非常适合应用内部和系统广播场景。如果确实需要接收其他应用的广播,再用RECEIVER_EXPORTED,但那样会暴露接收能力,要额外注意安全。
3.2 静态注册实战:清单文件里的小本本
静态注册是在AndroidManifest.xml里通过<receiver>标签声明接收器,相当于你给应用装了一个“永久喇叭”——即使应用进程被杀掉,系统也可能通过反射把进程拉起来,让接收器处理广播。
最常见的静态注册场景是开机自启,先申请权限:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />再注册接收器:
<receiver android:name=".BootReceiver" android:enabled="true" android:exported="true"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver>对应接收器代码:
public class BootReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { // 开机完成后拉起核心服务 Intent service = new Intent(context, CoreService.class); context.startForegroundService(service); } } }这里必须说的一个重点:Android 8.0(API 26)之后,绝大多数隐式广播已经不支持静态注册了。系统做了个大改革,把所有静态注册的隐式广播拉黑,只剩下一小撮例外还开着绿灯,比如:
BOOT_COMPLETED:开机完成LOCKED_BOOT_COMPLETED:解锁前的开机完成MY_PACKAGE_REPLACED:应用自身被覆盖安装ACTION_SHUTDOWN:关机LOCALE_CHANGED:系统语言变化USB_ACCESSORY_ATTACHED/USB_DEVICE_ATTACHED:USB设备插拔
这些之外的通通不行。你可以静态注册自定义Action吗?在Android 8.0以下可以,8.0以上就不再收到隐式广播了。不过如果你发的是显式广播(用setPackage(getPackageName())限制为自家应用,或者直接用ComponentName指定接收器),静态注册依然有效。这种设计是为了防止恶意应用在后台频繁被拉起,保护电量。
还有一个坑:targetSdkVersion在31(Android 12)及以上时,凡是带有<intent-filter>的四大组件,必须显式声明android:exported属性。BootReceiver这种需要接收系统广播的,必须设置为true;如果只给自己应用发广播,就设false。不写这个属性,安装直接报错。
3.3 动态与静态怎么选:一张表看清
| 对比维度 | 动态注册 | 静态注册 |
|---|---|---|
| 注册位置 | 代码里registerReceiver | AndroidManifest.xml |
| 生命周期 | 跟随组件生命周期,需注销 | 常驻,进程被杀死也可能拉起 |
| 接收隐式广播 | 支持 | Android 8.0起大部分不支持 |
| 系统广播监听 | 适合屏幕亮灭、时间变化等高频 | 只适合开机、安装、语言切换等低频 |
| 内存泄漏风险 | 不注销会泄漏 | 无此风险 |
| 耗电影响 | 注册期间才接收 | 常驻,系统负担大 |
我的习惯是:能动态注册就不静态注册。动态注册把接收器的生命力和组件绑死,不会出现Activity已经销毁了还傻傻处理广播的情况。只有开机自启、应用更新后重新调度、UX自动安装这类系统级事件,才考虑静态注册。
4. 按作用范围划分:本地广播与全局广播
广播按作用范围还能分成本地广播和全局广播。在Android开发的历史里,本地广播曾是一个标准答案,后来又被官方亲手推翻,这个演进过程非常有意思。
4.1 本地广播:曾经的安全屋
本地广播是指广播只在应用内部传递,不会跑到应用外面去,其他应用收不到。早期的方案是LocalBroadcastManager,可以把它理解成一个私有化的消息总线,内部用Handler实现,效率比系统广播高,也能防止外部应用嗅探你的消息。
使用方式如下:
// 注册 LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filter); // 发送 LocalBroadcastManager.getInstance(this).sendBroadcast(intent); // 注销 LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);这套API用起来很简便,内部自己走Handler,不走AMS,所以性能比全局广播好,也不会把消息泄漏到外部应用。
但是,LocalBroadcastManager已经被官方标记为废弃了。androidx.localbroadcastmanager包最后更新停留在1.1.0,Google明确建议开发者迁移到LiveData或Flow。为什么废弃?因为它的作用就是“应用内一对一、一对多通信”,而LiveData和协程Flow完全可以覆盖这些场景,还天然支持生命周期感知,不需要手动注册注销,也不用手动处理线程切换。继续在新项目里用LocalBroadcastManager没什么技术上的好处,反而会被code review的人问一句“这里为什么不直接用LiveData”。
4.2 现在的替代方案:LiveData与SharedFlow
如果只是想应用内部发个消息,我的建议是分情况选:
需求是UI层感知数据变化,直接用LiveData。登录状态、列表刷新、用户资料更新,这些都是典型的UI状态变更,LiveData配合ViewModel用起来很顺手:
class MainViewModel : ViewModel() { private val _refreshEvent = MutableLiveData<Boolean>() val refreshEvent: LiveData<Boolean> = _refreshEvent fun triggerRefresh() { _refreshEvent.value = true } } // Activity 里观察 viewModel.refreshEvent.observe(this) { needRefresh -> if (needRefresh) loadData() }如果需求是“非UI层的一次性事件”,比如网络库收到推送想通知仓库层更新缓存,更推荐用SharedFlow。SharedFlow是热流,不管有没有订阅者都会把事件发出去,而且支持replay让新订阅者拿到最近一条事件,比LiveData处理一次性事件要灵活:
class MessageBus { companion object { val refreshFlow = MutableSharedFlow<String>(extraBufferCapacity = 10) } } // 发送 MessageBus.refreshFlow.emit("refresh_now") // 收集 lifecycleScope.launch { MessageBus.refreshFlow.collect { msg -> Log.d("Bus", "收到: $msg") } }那全局广播还用来干嘛?跨应用通信,或者系统事件监听。比如你的应用要接收另一个应用发的数据包,或者要响应开机、安装、网络切换这些系统事件,还得走sendBroadcast()。但注意安全:全局广播默认是“广播出去谁都能听”,如果你发的是应用内的敏感信息,一定要setPackage(getPackageName())把自己锁定,或者用显式广播直接指定接收组件。
显式广播的正确姿势:
Intent intent = new Intent("com.example.myapp.INTERNAL_ACTION"); intent.setPackage(getPackageName()); // 只允许本应用接收 context.sendBroadcast(intent);这样的话,即使有个恶意应用监听所有Action,它也会因为包名对不上而不是你的目标。
5. 系统广播与自定义广播:两类高频实操对象
广播类别里除了按发送方式、注册方式、作用范围划分,还有一个特别重要的角度:广播事件本身是谁定义的。这里分系统广播和自定义广播。
5.1 系统广播常用Action清单
系统广播是安卓系统在各个关键节点发出来的事件,接收方写好Action字符串就能监听。我梳理了一份日常开发里出现频率最高的清单:
| Action | 触发时机 | 注册方式 | 备注 |
|---|---|---|---|
ACTION_BOOT_COMPLETED | 系统启动完成 | 静态 | 需要RECEIVE_BOOT_COMPLETED权限 |
ACTION_TIME_TICK | 每分钟时间变化 | 动态 | 不能静态注册 |
ACTION_SCREEN_ON/ACTION_SCREEN_OFF | 屏幕亮/灭 | 动态 | 不能静态注册 |
ACTION_BATTERY_LOW/ACTION_BATTERY_OKAY | 电量低/恢复 | 动态 | 不能静态注册 |
ACTION_PACKAGE_ADDED/ACTION_PACKAGE_REMOVED | 应用安装/卸载 | 动态或静态 | 静态注册受限,需指定scheme |
ACTION_HEADSET_PLUG | 耳机插拔 | 动态 | 传统方案,新版有线音频建议用AudioManager |
ACTION_CONNECTIVITY_CHANGE | 网络连接变化 | 动态 | 官方已不建议监听,用ConnectivityManager回调更稳 |
ACTION_MY_PACKAGE_REPLACED | 自身应用更新 | 静态 | 更新后做数据迁移常用 |
ACTION_LOCALE_CHANGED | 系统语言变化 | 静态 | 需重新初始化资源 |
ACTION_SHUTDOWN | 系统关机 | 静态 | 做收尾工作 |
注意几个细节:
ACTION_TIME_TICK、ACTION_SCREEN_ON/OFF只能动态注册,静态注册写了也白写。ACTION_CONNECTIVITY_CHANGE虽然可以动态注册,但官方更推荐使用ConnectivityManager.NetworkCallback,毕竟现在网络类型五花八门,单纯监听Action拿不到足够的状态信息。- 监听
ACTION_PACKAGE_ADDED这类跟包相关的广播,要在IntentFilter里加addDataScheme("package"),不然收不到:
IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); filter.addDataScheme("package"); registerReceiver(packageReceiver, filter);系统广播是理解安卓事件驱动模型的最佳入口。建议新手自己动手写一个小Demo,动态注册监听屏幕亮灭和电量变化,日志里观察事件触发顺序,比死记硬背清单有效得多。
5.2 自定义广播的Action规范与发送
除了听系统广播,应用之间还能自己定义Action来通信。自定义广播的Action字符串不是随便写个"hello"就行,行业里通常用包名做前缀,格式类似:
com.example.myapp.CUSTOM_ACTION这样能避免跟其他应用的Action撞车。比如你要写一个下载完成的广播:
// 发送 Intent intent = new Intent("com.example.myapp.DOWNLOAD_COMPLETE"); intent.putExtra("fileId", 10086); intent.setPackage(getPackageName()); sendBroadcast(intent);// 接收 public class DownloadReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if ("com.example.myapp.DOWNLOAD_COMPLETE".equals(intent.getAction())) { int fileId = intent.getIntExtra("fileId", -1); // 更新数据库 // 发通知栏提醒 } } }自定义广播如果是给自己应用用的,强烈建议加上setPackage(getPackageName())收窄范围。如果不加,等于把内部消息明文广播给整个系统,任何安装了监听器的应用都能顺手偷走你的数据。现在很多App通信都改走深链、互拉等方案,但自定义广播在处理同一个应用不同进程之间的通信时仍然有用,比如多进程架构下通知另一个进程刷新数据。
5.3 粘性广播:一个埋掉的雷
老早以前的安卓版本还有一个叫粘性广播(Sticky Broadcast)的分类,发送方式是sendStickyBroadcast()。它的特点是广播发送完之后不会立刻消失,而是像口香糖一样粘在系统里,之后注册的新接收器也能立刻收到这条广播。
听起来好像很美好,但问题是它存在严重的安全和隐私漏洞:任何应用都能拿到你发的粘性数据,容易造成敏感信息泄露。这套API早就被标记废弃了,现在的新项目里见到它的概率极低。如果你在维护老项目遇到sendStickyBroadcast(),建议尽快替换成正常的广播或者LiveData。面试时如果被问到,直接说“已经废弃的机制,不用”就够了,不用花太多时间研究。
6. 常见问题与排查技巧实录
6.1 广播没收到:从注册、发送到匹配的逐个排查
广播没收到,是新手问烂了的问题。我每次排查都按这个顺序来:
第一步,检查Action字符串是否完全一致。别小看这个,多一个空格、大小写不同都会匹配失败。Action的推荐写法是包名加常量,然后发送和接收都引用同一个常量,避免拼写错误。
第二步,检查注册是否成功。动态注册要看代码有没有执行到registerReceiver,比如放在某个按钮回调里,但你压根没点过那个按钮;或者注册了又注销了,时序对不上。静态注册要确认AndroidManifest.xml里的<receiver>标签没写错,包名路径对不对。
第三步,检查Android版本限制。如果你静态注册了一个自定义Action,然后在API 26以上的设备测试,收不到是正常的,因为系统不允许这种玩法。
第四步,检查应用是否被“停止”了。用户在系统设置里强行停止应用后,应用进程被冻结,静态注册的接收器也不会被唤醒。这是设计如此,别觉得是Bug。
第五步,检查Exported标志。targetSdkVersion31之后,接收器没写android:exported或动态注册没传FLAG,直接抛异常或者收不到,去logcat里搜索Exception关键字能快速看到原因。
还有一个经常被忽略的场景:发送时机早于注册时机。比如你在onCreate()里发广播,接收器在onResume()里才注册,那这条广播发出时系统根本不知道有接收者,自然收不到。解决思路是调整注册时机,或者用第4部分提到的LiveData/Flow,它们不依赖注册时序。
6.2 动态注册的泄漏问题:Activity已经没了还在收
这个坑多见于Activity里注册了接收器,却在onDestroy()里忘记注销。结果Activity实例明明已经被回收了,但因为BroadcastReceiver还持有它的引用,导致GC无法回收整棵对象树,时间一长就OOM。
正确的绑定方式是:onStart()里注册,onStop()里注销;或者onResume()里注册,onPause()里注销。把注册注销放在onCreate()和onDestroy()看起来对称很美观,但风险很大,特别是用户按Home键退出时Activity只是stop并没有destroy,广播依然在收,白白耗电。
如果你的接收器需要处理耗时逻辑,有两种改进思路:要么让onReceive()里发个消息给WorkManager去执行任务;要么直接用goAsync()告诉系统我需要一点时间处理,等处理完再调用PendingResult.finish()。但goAsync()只适合延长几秒,不适合跑长任务。
@Override public void onReceive(Context context, Intent intent) { final PendingResult result = goAsync(); new Thread(() -> { // 耗时操作 result.finish(); }).start(); }不要在主线程里开线程就以为万事大吉了,goAsync()之后仍然有超时限制,官方没规定死时间,但超过十秒一样危险。真要跑长任务,老老实实startService()或者WorkManager。
6.3 优先级不生效:先确认是不是有序广播
“我设置了high priority,为什么没抢到先手?”这个问题我回答过很多次。优先级只在有序广播里才有意义。用sendBroadcast()发的是标准广播,所有接收者之间无序无优先级可言;如果用的是sendOrderedBroadcast(),还要注意优先级数字是否真的写进去了。
动态注册设置优先级:
IntentFilter filter = new IntentFilter("com.example.myapp.ORDERED_ACTION"); filter.setPriority(500);静态注册设置优先级:
<intent-filter android:priority="500"> <action android:name="com.example.myapp.ORDERED_ACTION" /> </intent-filter>另一个常见迷惑点是,两个接收器优先级相同怎么办?同优先级下系统不保证顺序,按注册先后或系统内部调度来。所以不要依赖优先级解决所有排序问题,真需要严格顺序就把上一个接收器处理完的结果通过setResultData()传给下一个,用数据驱动下一个动作。
6.4 常见广播问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 自定义广播收不到 | Action不一致 / 参数错误 | 统一常量;检查setPackage |
| 开机广播收不到 | 未声明权限 / 应用被停止 / 机型限制 | 加RECEIVE_BOOT_COMPLETED;引导用户加入自启动白名单 |
| 屏幕广播静态注册没反应 | 此广播不允许静态注册 | 改成动态注册 |
| 动态注册报SecurityException | targetSdk 34未传FLAG | 补上RECEIVER_NOT_EXPORTED/RECEIVER_EXPORTED |
| 有序广播后续接收器没收到 | 前一个接收器调用了abortBroadcast | 确认业务是否需要拦截 |
| 多次快速发送广播丢消息 | 系统广播分发是异步的,接收器太重 | 减少广播频率,或改用消息队列 |
| Activity泄漏 | 未注销接收器 | onStop/onPause里unregisterReceiver |
6.5 别在onReceive里做的三件事
最后再列几个我在实际项目里看到过的高危操作,都是真实踩出来的坑:
第一,别弹对话框。onReceive()执行时可能根本没有Activity在前台,直接弹Dialog会出现BadTokenException或者对话框显示在别的应用上面。要提醒用户就去发通知栏通知。如果想启动一个Activity,记得加FLAG_ACTIVITY_NEW_TASK:
Intent activityIntent = new Intent(context, ResultActivity.class); activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(activityIntent);第二,别绑定Service。onReceive()里调用bindService()会报“Context.bindService() called from BroadcastReceiver without using goAsync()”错误,因为接收器生命周期太短,Service绑定还没完成接收器就没了。改成startService()或startForegroundService(),并注意Android 8.0后台启动Service的限制。
第三,别用广播做高频率通信。比如拖动进度条每秒发N条广播,接收器再更新UI,性能和流畅度会非常感人。这种情况下高度怀疑你在做重复造轮子:同一个应用内部数据共享,用LiveData、StateFlow或者直接回调接口才是正路。广播的定位是“松耦合的全局事件通知”,不是“内部数据总线”。
说实话,广播这套东西本身不难,难就难在版本演进后遗留的各种“历史兼容问题”和“边界情况”。我个人的习惯是:先判断消息作用域,应用内部优先LiveData/Flow;需要跟系统或其他应用联动才考虑广播;需要开机自启、应用更新等后台事件时静态注册;其他能动态就动态。按这条规则走下来,90%的广播坑都能绕开。
这次先把广播的类别理清了,下一期我会接着聊广播的进阶内容,包括自定义权限、前台广播受限的具体处理、以及多进程通信里的广播坑。你在项目里碰到过什么广播相关的奇葩问题,可以在评论区唠唠,我挑典型的下期展开。