ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android BroadcastReceiver 六种广播类型详解与实战避坑指南

Android BroadcastReceiver 六种广播类型详解与实战避坑指南

Android 开发做久了,BroadcastReceiver 这玩意儿你一定绕不开。它算是四大组件里最“轻”的一个,没有界面、生命周期极短,但偏偏在系统事件分发、跨页面通信、模块解耦这些场景里出镜率极高。我见过不少项目,Activity 之间传个数据都要靠广播绕一圈,也见过有人把广播用成了 EventBus,结果代码越写越乱。问题往往不在广播本身,而在于没搞清楚它到底有几种形态、各自适合什么场景、坑埋在哪里。

这篇就把 BroadcastReceiver 的几种典型用法一次讲透:普通广播、有序广播、拦截广播、本地广播、Sticky 广播,以及系统广播。每一种我都会说清楚它的行为特征、适用场景、代码怎么写,以及我在实际项目里踩过的坑。不管你是刚接触 Android 的新手,还是写了几年但一直没系统梳理过广播的老手,看完应该都能有点收获。尤其是本地广播和 Sticky 广播这两块,很多人只知道有这么个东西,真到用的时候才发现细节全是坑。

1. 先把广播的两种注册方式理清楚

在聊具体类型之前,得先把注册方式说清楚,因为后面所有广播的行为都跟注册方式强相关。Android 里注册广播只有两条路:静态注册和动态注册。这两者的差异不只是“写在 Manifest 里还是写在代码里”这么简单,它直接决定了广播能不能收到、什么时候能收到、以及在新版本系统上会不会被限制。

1.1 静态注册:写在 Manifest 里的常驻监听

静态注册就是在AndroidManifest.xml里通过<receiver>标签声明。这种注册方式的特点是:应用没启动也能被系统拉起来接收广播(前提是广播能匹配上)。早期很多应用靠这个做开机自启、后台唤醒,后来系统版本一路收紧,静态注册能收到的隐式广播越来越少。

<receiver android:name=".MyStaticReceiver" android:exported="true"> <intent-filter> <action android:name="com.example.MY_ACTION" /> </intent-filter> </receiver>

这里有个必须注意的点:从 Android 8.0(API 26)开始,绝大多数隐式广播不再允许静态注册接收。也就是说,你写了个静态接收器监听某个自定义 action,如果发送方用的是隐式 Intent,那在 8.0 以上的设备上大概率收不到。系统这么做的目的是省电、防止应用被随意唤醒。所以现在静态注册基本只用于接收少数系统白名单广播,比如开机完成、时区变化这类。

提示:android:exported属性在 Android 12 之后是强制要求显式声明的,不写会直接编译报错。如果这个接收器只接收应用内部广播,务必设为false,避免被外部应用触发。

1.2 动态注册:跟着组件生命周期走

动态注册是在代码里调用registerReceiver(),通常在 Activity 或 Service 的onCreate/onResume里注册,在对应的onDestroy/onPause里注销。它的特点是灵活、可控,而且不受 8.0 隐式广播限制的影响——动态注册的接收器依然能收到大部分隐式广播。

IntentFilter filter = new IntentFilter(); filter.addAction("com.example.MY_ACTION"); myReceiver = new MyReceiver(); registerReceiver(myReceiver, filter);

动态注册最大的坑就是忘记注销。注册了不注销,轻则内存泄漏,重则onReceive在组件已经销毁后还被调用,直接崩溃。我个人的习惯是:注册和注销成对写,注册写在onCreate,注销写在onDestroy,中间不要穿插条件判断,避免某条分支漏掉注销。

对比项静态注册动态注册
注册位置Manifest 文件代码中
生命周期随应用进程随注册组件
8.0 隐式广播限制受限不受限
是否需手动注销否是
典型场景开机、时区等系统事件页面内、Service 内通信

理解了这两种注册方式,后面的内容就好展开了。因为广播的类型差异,本质上是在“发送方式”和“接收范围”上做文章。

2. 普通广播:最基础也最容易被滥用的那一种

普通广播(Normal Broadcast)就是最常规的sendBroadcast()。它的行为特征很明确:所有匹配的接收器都会收到,接收顺序不确定,接收器之间互不影响,一个接收器出问题不影响其他接收器。听起来很美好,但恰恰因为“谁都能收、谁都能发”,它成了最容易被滥用的通信方式。

2.1 普通广播的发送与接收链路

发送一个普通广播非常简单:

Intent intent = new Intent("com.example.NORMAL_ACTION"); intent.putExtra("data", "hello"); sendBroadcast(intent);

接收端只要 action 匹配就能收到:

public class MyReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String data = intent.getStringExtra("data"); // 处理数据 } }

这里有个关键点很多人忽略:onReceive运行在主线程。这意味着你不能在里面做耗时操作,超过 10 秒会 ANR。如果确实需要处理耗时逻辑,正确做法是在onReceive里启动一个 Service 或者用goAsync()配合线程池处理。我见过有人在onReceive里直接查数据库、发网络请求,结果线上 ANR 率飙升,排查半天才发现是广播惹的祸。

2.2 普通广播的适用边界与滥用后果

普通广播适合什么场景?我的判断标准是:发送方和接收方完全解耦、接收方数量不确定、且对接收顺序没有要求。比如某个数据同步完成后通知多个模块刷新,这种场景用普通广播是合理的。

但很多项目把它用成了“万能通信总线”:Activity 给 Fragment 传数据用广播、点击事件用广播、甚至按钮状态更新也用广播。结果就是代码里到处是sendBroadcast,接收器满天飞,改一个 action 要全局搜索,维护成本极高。更麻烦的是,普通广播是全局的,任何应用只要知道 action 就能监听(动态注册的情况下),存在数据泄露风险。

注意:如果广播携带敏感数据,普通广播的全局可见性就是个隐患。这种情况下应该优先考虑本地广播,或者给广播加上权限校验。

我的经验是:应用内部的通信,能用接口回调、ViewModel、LiveData 解决的,就别用广播。广播留给那些真正需要跨组件、跨进程、或者跟系统事件打交道的场景。

3. 有序广播:能改数据、能拦截的“串行分发”

有序广播(Ordered Broadcast)跟普通广播最大的区别在于:接收器是按优先级顺序逐个接收的,而且前面的接收器可以修改数据、甚至可以完全拦截广播,让后面的接收器收不到。这个特性决定了它的使用场景跟普通广播完全不同。

3.1 优先级机制与数据传递

发送有序广播用sendOrderedBroadcast():

Intent intent = new Intent("com.example.ORDERED_ACTION"); sendOrderedBroadcast(intent, null);

接收器的优先级通过IntentFilter的setPriority()设置,数值越大优先级越高:

IntentFilter filter = new IntentFilter("com.example.ORDERED_ACTION"); filter.setPriority(100); registerReceiver(highPriorityReceiver, filter);

高优先级的接收器先收到广播,它可以通过setResultData()、setResultExtras()修改结果数据,后面的接收器通过getResultData()、getResultExtras()拿到修改后的数据。这就形成了一条数据处理链:A 处理完传给 B,B 处理完传给 C。

这里有个细节:动态注册的接收器优先级高于静态注册的,同等优先级下动态注册的先收到。这个规则在排查“为什么我的接收器没先收到”时很有用。

3.2 拦截广播:abortBroadcast 的正确用法

拦截广播靠的是abortBroadcast():

@Override public void onReceive(Context context, Intent intent) { if (shouldBlock) { abortBroadcast(); return; } // 正常处理 }

调用abortBroadcast()后,优先级比当前接收器低的接收器就收不到这条广播了。这个机制在系统里有实际应用,比如短信拦截类应用就是靠这个实现的(当然现在系统权限收紧了,普通应用很难再这么干)。

但这里有个大坑:abortBroadcast()只对有序广播有效。如果你对一个普通广播调用它,会直接抛RuntimeException。我当年就犯过这个错,把一个普通广播的接收器改成了有序广播的发送方式,结果忘了改abortBroadcast的调用条件,线上直接崩。所以用拦截功能前,一定要确认发送方用的是sendOrderedBroadcast。

特性普通广播有序广播
发送方法sendBroadcastsendOrderedBroadcast
接收顺序不确定按优先级
能否修改数据否能
能否拦截否能(abortBroadcast)
适用场景通知类、无顺序要求数据处理链、拦截

有序广播的性能比普通广播差一些,因为要串行分发。所以如果只是单纯通知,没必要用有序广播,普通广播就够了。

4. 本地广播:应用内通信的安全选择

本地广播(Local Broadcast)是 Android Support 库(后来并入 AndroidX)提供的一个工具,专门解决普通广播的两个痛点:全局可见导致的安全问题,以及跨应用广播带来的性能开销。它的核心特点是:广播只在应用内部传递,不会离开当前应用的进程。

4.1 LocalBroadcastManager 的工作机制

使用本地广播需要先拿到LocalBroadcastManager实例:

LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context); // 注册 IntentFilter filter = new IntentFilter("com.example.LOCAL_ACTION"); manager.registerReceiver(localReceiver, filter); // 发送 Intent intent = new Intent("com.example.LOCAL_ACTION"); manager.sendBroadcast(intent); // 注销 manager.unregisterReceiver(localReceiver);

它的内部实现其实不是真正的广播机制,而是基于 Handler 的消息传递。发送方把 Intent 封装成消息投递到主线程的 Handler,接收器在主线程回调。所以它本质上是一个“披着广播外衣的进程内事件总线”。

这个实现方式带来几个好处:第一,广播不出应用,其他应用无法监听,安全性高;第二,不需要跨进程通信(IPC),性能比普通广播好;第三,不受系统对隐式广播的限制影响。

4.2 本地广播的局限与替代方案

但本地广播也不是没有缺点。首先,它已经被标记为废弃(deprecated),官方推荐用LiveData或者响应式流来替代进程内通信。其次,它只能在应用内使用,跨进程场景完全用不了。再者,它的 API 设计跟系统广播类似,容易让人误以为可以跨应用,造成理解混乱。

我在实际项目里的做法是:新项目直接用LiveData或者Flow做进程内通信,不再引入LocalBroadcastManager。如果是维护老项目,遇到已有的本地广播代码,能不动就不动,但新写的模块不会再往里加。如果非要用广播做进程内通信,本地广播确实比普通广播安全,但要注意它的废弃状态,别在新技术选型里再选它。

提示:本地广播的onReceive同样运行在主线程,耗时操作一样要避免。这一点跟普通广播没有区别。

5. Sticky 广播:发送后注册也能收到的“粘性”机制

Sticky 广播是个挺特殊的存在。普通广播是“发送时注册了才能收到”,而 Sticky 广播是“发送后,即使接收器还没注册,等它注册时依然能收到之前发送的那条广播”。这个特性听起来很实用,但实际用起来坑非常多,而且在新版本系统上已经被废弃。

5.1 Sticky 广播的发送与获取

发送 Sticky 广播用sendStickyBroadcast():

Intent intent = new Intent("com.example.STICKY_ACTION"); intent.putExtra("battery", 88); sendStickyBroadcast(intent);

接收端注册后,系统会立即把最后一条 Sticky 广播回调给它:

IntentFilter filter = new IntentFilter("com.example.STICKY_ACTION"); registerReceiver(stickyReceiver, filter);

也可以主动去“捞”最后一条 Sticky 广播:

Intent stickyIntent = registerReceiver(null, filter); if (stickyIntent != null) { int battery = stickyIntent.getIntExtra("battery", -1); }

传null作为接收器,就是只查询不注册,这个用法在获取电量、充电状态时比较常见。

5.2 为什么 Sticky 广播被废弃了

Sticky 广播被废弃的原因主要有几个。第一,它需要SEND_STICKY_BROADCAST权限,而这个权限是系统级的,普通应用根本申请不到,所以实际上只有系统应用能用。第二,Sticky 广播会一直保留在系统里,占用内存,而且数据可能过期,接收方拿到的是“历史数据”而不是“当前状态”,容易造成逻辑错误。第三,它的行为不够直观,很多开发者搞不清楚什么时候能收到、什么时候收不到,调试成本高。

现在获取系统状态(比如电量、网络状态)都有更规范的 API,比如BatteryManager、ConnectivityManager,不需要再依赖 Sticky 广播。所以我的建议很直接:新项目不要用 Sticky 广播,老项目里如果遇到了,评估一下能不能换成对应的系统 API。

特性普通广播Sticky 广播
注册前发送能否收到否能
是否需要特殊权限否是(系统级)
数据时效性实时可能是历史数据
当前状态正常使用已废弃

6. 系统广播:跟系统事件打交道的正确姿势

系统广播是 Android 系统在特定事件发生时发出的广播,比如开机完成、网络变化、电量变化、屏幕开关等。开发者通过监听这些广播,可以让应用对系统状态做出响应。但系统广播这块的坑,比前面几种加起来都多,主要原因是系统版本一直在收紧限制。

6.1 常见系统广播与版本适配

先列几个常用的系统广播 action:

广播 Action触发时机注册方式建议
ACTION_BOOT_COMPLETED开机完成静态注册
ACTION_TIMEZONE_CHANGED时区变化静态注册
ACTION_CONNECTIVITY_CHANGE网络变化动态注册
ACTION_BATTERY_CHANGED电量变化动态注册(Sticky)
ACTION_SCREEN_ON/OFF屏幕开关动态注册

这里的关键适配点是 Android 8.0 的隐式广播限制。除了少数白名单广播(如开机、时区),大部分系统广播不再允许静态注册接收。所以像网络变化、电量变化这类,必须改成动态注册。如果你的应用依赖静态注册监听网络变化,在 8.0 以上设备上会完全失效,这个坑我踩过,排查了半天才发现是版本限制。

6.2 系统广播监听的性能与合规考量

监听系统广播不是越多越好。每多监听一个广播,应用被唤醒的机会就多一次,耗电也就上去了。尤其是静态注册的广播,应用进程可能因为一条广播被拉起,用户感知就是“这应用怎么老在后台跑”。所以我的原则是:只监听真正需要的系统广播,能用其他 API 替代的就替代。

比如网络状态,与其监听ACTION_CONNECTIVITY_CHANGE,不如用ConnectivityManager.registerDefaultNetworkCallback(),更精准也更省电。再比如电量,BatteryManager提供的 API 比监听广播更直接。系统广播适合那些“没有更好替代方案”的场景,比如开机自启、时区变化这种。

注意:从 Android 10 开始,后台启动 Activity 受到严格限制,很多依赖系统广播拉起界面的做法已经行不通了。设计功能时要把这个限制考虑进去,别做出一个在新系统上根本跑不起来的方案。

7. 几种广播的选型对照与实战避坑清单

聊完这几种广播,最后做个选型对照,再把我这些年踩过的坑集中列一下。选型这件事没有绝对的对错,关键是匹配场景。

7.1 一张表搞定广播选型

场景推荐方案理由
应用内模块解耦通信LiveData / Flow比广播更轻、更安全
应用内必须用广播本地广播不出应用,安全
通知多个模块,无顺序要求普通广播简单直接
需要按顺序处理、可拦截有序广播支持优先级和 abort
获取系统最后状态对应系统 APISticky 已废弃
监听系统事件动态注册 + 白名单静态适配 8.0 限制

选型的核心逻辑就一句话:能用更轻量的方案就别用广播,非要用广播就选范围最小的那种。本地广播比普通广播范围小,普通广播比全局隐式广播范围小,层层收窄,安全性和性能都更好。

7.2 我踩过的那些广播坑

第一个坑:onReceive里做耗时操作导致 ANR。这个前面提过,但真的太多人中招。记住onReceive在主线程,超过 10 秒就 ANR,耗时逻辑要么丢给 Service,要么用goAsync()。

第二个坑:动态注册忘记注销导致内存泄漏。尤其是把接收器注册在 Activity 里,Activity 销毁了接收器还活着,持有 Activity 引用,泄漏就这么来的。成对写注册注销,别偷懒。

第三个坑:对普通广播调用abortBroadcast()导致崩溃。这个错误很隐蔽,因为编译期不报错,运行到那行才崩。用拦截功能前确认发送方是有序广播。

第四个坑:静态注册监听隐式广播在 8.0 以上失效。这个坑的迷惑性在于,低版本设备上一切正常,高版本设备上静默失效,没有任何报错。适配时一定要在目标版本上实测。

第五个坑:Sticky 广播数据过期导致逻辑错误。接收方拿到的是历史数据,如果业务逻辑假设这是当前状态,就会出问题。能用系统 API 就别用 Sticky。

第六个坑:广播携带敏感数据被外部应用监听。普通广播是全局的,动态注册的接收器任何应用都能注册监听。敏感数据要么用本地广播,要么加密,要么加权限校验。

7.3 关于 goAsync 的一个实用技巧

最后分享一个goAsync()的用法,这个在需要异步处理广播时很有用:

@Override public void onReceive(Context context, Intent intent) { final PendingResult pendingResult = goAsync(); new Thread(() -> { try { // 耗时操作 doSomethingSlow(); } finally { pendingResult.finish(); } }).start(); }

goAsync()会返回一个PendingResult,告诉系统“我还在处理,别回收我”。处理完调用finish()释放。这样既避免了 ANR,又不会因为onReceive返回后广播被回收导致处理中断。注意finish()一定要在finally里调用,否则异常路径下会泄漏。

广播这东西,说简单也简单,说坑多也是真多。我的整体态度是:把它当成一个“系统级的事件通知机制”来用,而不是“应用内的通信总线”。应用内通信有太多比广播更合适的方案,广播的价值在于跟系统打交道、以及跨进程这种特殊场景。把范围收窄,把注册注销管好,把主线程意识刻在脑子里,广播用起来就不会出大问题。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进