news 2026/9/9 19:24:08

Android广播机制全解析:标准/有序/动态/静态注册一次理清

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android广播机制全解析:标准/有序/动态/静态注册一次理清

这篇是安卓基础系列的第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 两者怎么选:先问自己三个问题

项目里遇到一个场景,先别急着写代码,问自己三个问题:

  1. 这条消息需要让多个接收方处理吗?如果只需要单个组件响应,其实不一定用广播,本地回调或者事件总线可能更轻量。
  2. 各接收方之间有没有先后依赖?有依赖就上sendOrderedBroadcast,没依赖就标准广播。
  3. 接收方会不会有外部应用?如果广播只是应用内部消息,别用全局广播,优先考虑本地广播或者现代替代方案(第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_EXPORTEDContext.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 动态与静态怎么选:一张表看清

对比维度动态注册静态注册
注册位置代码里registerReceiverAndroidManifest.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_TICKACTION_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;引导用户加入自启动白名单
屏幕广播静态注册没反应此广播不允许静态注册改成动态注册
动态注册报SecurityExceptiontargetSdk 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%的广播坑都能绕开。

这次先把广播的类别理清了,下一期我会接着聊广播的进阶内容,包括自定义权限、前台广播受限的具体处理、以及多进程通信里的广播坑。你在项目里碰到过什么广播相关的奇葩问题,可以在评论区唠唠,我挑典型的下期展开。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 19:22:45

找位置:哈希映射与顺序输出的字符串处理技巧

刷题时遇到编号 3610 的“找位置”&#xff0c;第一反应是“这题名字取得也太朴素了”。等真正动手做了才发现&#xff0c;它几乎是字符串处理里最典型的一类题&#xff1a;给你一串字符&#xff0c;找出出现次数超过一次的字符&#xff0c;并且把每个字符出现过的所有位置都输…

作者头像 李华
网站建设 2026/9/9 19:21:59

Redis Cluster分片集群:槽位分配与读写路径详解

一台 Redis 撑不住数据量的时候怎么办&#xff1f;很多团队的第一反应是上主从复制&#xff0c;但其实主从模式只是解决了高可用和读扩展问题&#xff0c;每台机器依然保存全量数据&#xff0c;内存天花板并没有被打破。真正要把数据量水平拆分出去&#xff0c;让每个节点只保留…

作者头像 李华
网站建设 2026/9/9 19:19:54

【JAVA毕设源码分享】基于springboot智能在线预约挂号系统的设计与实现(程序+文档+代码讲解+一条龙定制)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/9/9 19:17:43

Python列表字典函数实战:手把手教你写一个班级管理系统

不知道有多少人在学完Python的列表、字典、函数之后&#xff0c;卡在了同一个问题上&#xff1a;这些语法点单独看都懂&#xff0c;但组合起来能干什么&#xff1f;我当年学Python也有这个阶段&#xff0c;后来靠“班级管理系统”这个小项目彻底把基础语法打通了。这个项目几乎…

作者头像 李华
网站建设 2026/9/9 19:16:48

MetaEditor命令行批量编译MT4/MT5 EA:从手工到自动化实战

做EA开发和量化交易的人&#xff0c;应该都有过这种体验&#xff1a;项目文件夹里有几十个.mq4或.mq5文件&#xff0c;改完一个公共的.mqh头文件&#xff0c;然后打开MetaEditor&#xff0c;一个个手动编译&#xff1b;编译完还得挨个确认左下角是不是真的出现了“0 errors, 0 …

作者头像 李华