Android 广播实战:从系统广播监听到自定义、有序与跨应用事件分发
前言
广播适合处理"状态变化已经发生,但接收方不想和发送方直接耦合"的场景。系统发出的电量、亮屏、息屏通知,或者应用内部的登录状态变化,本质上都属于这一类事件分发问题。整篇内容先从系统广播的动态注册入手,再延伸到静态注册的限制、自定义广播的发送与接收、有序广播的分发顺序控制,以及跨应用广播的访问边界。
阅读顺序与实现推进保持一致:先看广播为什么能承接状态通知,再完成系统广播监听,然后把同样的思路迁移到应用内登录状态同步,最后把接收器抽离并扩展到跨应用事件分发。
目录
- [Android 广播实战:从系统广播监听到自定义、有序与跨应用事件分发](#Android 广播实战:从系统广播监听到自定义、有序与跨应用事件分发)
- [1. 广播机制、分类与注册边界](#1. 广播机制、分类与注册边界)
- [1.1 广播为什么适合做状态通知](#1.1 广播为什么适合做状态通知)
- [1.2 标准广播与有序广播的区别](#1.2 标准广播与有序广播的区别)
- [1.3 动态注册与静态注册的使用边界](#1.3 动态注册与静态注册的使用边界)
- [2. 监听系统发出的广播消息](#2. 监听系统发出的广播消息)
- [2.1 系统广播场景与页面前置代码](#2.1 系统广播场景与页面前置代码)
- [2.2 点击按钮后动态注册系统广播接收器](#2.2 点击按钮后动态注册系统广播接收器)
- [2.3 按步骤验证电量低、息屏与亮屏广播](#2.3 按步骤验证电量低、息屏与亮屏广播)
- [3. 静态注册广播接收器及其限制](#3. 静态注册广播接收器及其限制)
- [3.1 把接收逻辑抽成独立的 BroadcastReceiver 类](#3.1 把接收逻辑抽成独立的 BroadcastReceiver 类)
- [3.2 在页面中复用独立接收器](#3.2 在页面中复用独立接收器)
- [3.3 清单静态注册与 android:exported 的含义](#3.3 清单静态注册与 android:exported 的含义)
- [4. 监听应用内的自定义广播消息](#4. 监听应用内的自定义广播消息)
- [4.1 登录状态广播要解决的问题](#4.1 登录状态广播要解决的问题)
- [4.2 测试页与登录页的前置界面](#4.2 测试页与登录页的前置界面)
- [4.3 发送登录状态广播](#4.3 发送登录状态广播)
- [4.4 接收登录事件并刷新页面状态](#4.4 接收登录事件并刷新页面状态)
- [5. 通过有序广播控制接收顺序](#5. 通过有序广播控制接收顺序)
- [5.1 将发送方式切换为 sendOrderedBroadcast](#5.1 将发送方式切换为 sendOrderedBroadcast)
- [5.2 使用优先级决定接收先后](#5.2 使用优先级决定接收先后)
- [5.3 有序广播适合解决什么问题](#5.3 有序广播适合解决什么问题)
- [6. 抽离接收器并扩展到跨应用广播](#6. 抽离接收器并扩展到跨应用广播)
- [6.1 把登录状态接收逻辑抽成公共接收器](#6.1 把登录状态接收逻辑抽成公共接收器)
- [6.2 页面如何接入抽离后的接收器](#6.2 页面如何接入抽离后的接收器)
- [6.3 跨应用发送自定义广播事件](#6.3 跨应用发送自定义广播事件)
- [7. 相关代码附录](#7. 相关代码附录)
- [7.1 系统广播页面与接收器](#7.1 系统广播页面与接收器)
- [7.2 自定义广播发送链路](#7.2 自定义广播发送链路)
- [7.3 登录状态接收器与页面接入](#7.3 登录状态接收器与页面接入)
1. 广播机制、分类与注册边界
1.1 广播为什么适合做状态通知
广播是一种面向事件的消息分发机制。发送方只需要把"发生了什么"封装进 Intent,系统再根据动作名、过滤条件和注册状态,把这条事件交给感兴趣的接收方。这样做的价值不在于替代页面跳转或方法调用,而在于把"状态变化"与"状态消费"分开。
像电量过低、屏幕熄灭、屏幕点亮这类系统事件,本来就由 Android 系统统一产生;应用内部的登录、退出登录、同步完成等业务状态,也经常需要同时通知多个页面。只要多个接收方对同一事件感兴趣,而发送方又不希望逐个持有它们的引用,广播就是一条成本较低的实现路径。
1.2 标准广播与有序广播的区别
广播的分发方式可以先分成两类:
- 标准广播是异步分发,多个接收器几乎同时收到事件,彼此之间没有固定先后关系。
- 有序广播是同步按优先级传递,优先级高的接收器会更早收到事件,后续接收顺序由
priority决定。
如果只是单纯通知"某个状态已经变化",标准广播已经足够;如果多个接收方之间存在明确的处理顺序,或者希望高优先级接收器先完成关键动作,再把事件交给后面的页面处理,就要切换到有序广播。
1.3 动态注册与静态注册的使用边界
注册方式同样需要先分清楚:
- 动态注册通过
registerReceiver(...)完成,接收能力依附于当前Activity或Service的生命周期。页面在,广播可收;页面销毁,广播就应该一起释放。 - 静态注册写在
AndroidManifest.xml中,接收器不依附某个具体页面,但 Android 8.0 以后对后台广播的限制越来越严格,很多系统广播已经不适合继续依赖静态注册。
因此,后面的实现会把动态注册作为主线。静态注册仍然会演示,但重点会放在它为什么受限、android:exported 为什么重要,以及高版本系统里为什么更推荐动态注册。
2. 监听系统发出的广播消息
2.1 系统广播场景与页面前置代码
当设备状态发生变化时,系统会主动向外广播事件。比如无线网络不可用、屏幕熄灭、屏幕点亮或者电量过低,应用只要提前声明自己关心哪些动作,就能在对应时刻收到通知,再决定是否刷新界面、提示用户或暂停某些操作。
当刷短视频时,WIFI 用不了了,app 会提醒当前 WIFI 无法使用,是否使用移动网络;
当打游戏时,电量低,此时弹出系统提示电量过低;
这些功能是通过系统给 app 发送通知,当系统状态发送变化,向 app 发送通知,app 根据监听到的系统状态结果,做对应的操作;这种操作就是广播;

页面先只保留一个"注册广播接收器"按钮,用它作为动态注册的触发入口。这样做的好处是,广播接收器并不会随着页面创建立刻注册,而是等到用户明确点击按钮之后,再开始监听目标事件。
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".SystemBroadcastActivity">
<Button
android:id="@+id/btn_regist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="注册广播接收器"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
BroadcastAndServiceByJavaProject/app/src/main/res/layout/activity_sysytem_broadcast.xml
按钮界面准备好之后,再把页面主体先搭出来。这里先声明一个成员变量 receiver,原因不是单纯为了保存对象,而是为了让后面的注销动作能拿到同一个接收器实例。只有注册和注销都指向同一对象,生命周期闭环才是完整的。
java
public class SystemBroadcastActivity extends AppCompatActivity {
private static final String TAG = "SystemBroadcastActivity";
private BroadcastReceiver receiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sysytem_broadcast);
//注册广播接收器
findViewById(R.id.btn_regist).setOnClickListener(view -> {
});
}
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/SystemBroadcastActivity.java
2.2 点击按钮后动态注册系统广播接收器
接下来真正进入广播接收链路。这里的动作顺序不能打乱,因为每一步都在给下一步准备上下文:
- 点击"注册广播接收器"按钮以后,才开始执行注册逻辑。
- 先创建
IntentFilter,因为接收器需要先知道自己关心哪些动作。 - 再通过
filter.addAction(...)把目标广播动作加入过滤器。 - 然后创建
BroadcastReceiver,把收到广播之后的处理逻辑写进onReceive(...)。 - 最后调用
registerReceiver(receiver, filter),把"接收器实例"和"动作过滤规则"一起交给系统。
这里过滤的动作一共三类:
- 电量低:
Intent.ACTION_BATTERY_LOW - 息屏:
Intent.ACTION_SCREEN_OFF - 亮屏:
Intent.ACTION_SCREEN_ON
回调触发后,真正决定业务分支的是 intent.getAction()。它返回当前到达的动作名,后面通过一组 if...else if... 判断,把电量低、息屏、亮屏拆成三条独立分支。这样写的价值在于,虽然三个事件共用同一个接收器,但每种系统状态都能落到各自的处理逻辑上,而不是混成一段统一判断。
java
//注册广播接收器
findViewById(R.id.btn_regist).setOnClickListener(view -> {
//意图过滤器
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_LOW);
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_SCREEN_ON);
//创建一个广播接收器
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
//处理接收到的广播信息
String action = intent.getAction();
//电池电量过低
if (action.equals(Intent.ACTION_BATTERY_LOW)) {
Log.i(TAG, "onReceive: 电池电量过低!");
//一般会做对应的UI操作
} else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
Log.i(TAG, "onReceive: 屏幕被关闭!");
} else if (action.equals(Intent.ACTION_SCREEN_ON)) {
Log.i(TAG, "onReceive: 屏幕点亮!");
}
}
};
// SystemBroadcastReceiver broadcastReceiver = new SystemBroadcastReceiver();
// registerReceiver(broadcastReceiver, filter);
//注册广播接收器
registerReceiver(receiver, filter);
});
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(receiver);
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/SystemBroadcastActivity.java
这段代码里有几个细节需要单独看清:
IntentFilter的职责是"先过滤,后分发"。没有加入过滤器的动作,即使系统发出来,当前页面也不会收到。receiver被定义成成员变量,不是为了写法统一,而是为了保证onDestroy()能用同一个实例执行unregisterReceiver(receiver)。onReceive(Context context, Intent intent)的两个参数承担不同作用。context代表当前回调可用的上下文;intent才是实际携带广播动作与附加数据的载体。intent.getAction()返回的是当前广播的动作字符串,后面的每一个分支判断都依赖这个返回值。unregisterReceiver(receiver)属于资源释放动作。如果页面销毁后仍然保留注册状态,轻则造成无效监听,重则在重复进入页面时出现接收器泄漏或重复接收。
2.3 按步骤验证电量低、息屏与亮屏广播
执行程序,点击注册按钮,此时就注册好了广播接收器:

注册成功以后,先模拟电量过低场景。验证顺序同样不能省略,因为这里不仅是在看结果,还在确认"过滤器是否生效"和"分支判断是否命中"。
根据下图的操作步骤,来模拟手机电量低的情况:

电量低,此时触发了广播接收器回调,并且判断广播类型是电量低,然后打印相关日志:

接着再验证屏幕状态切换。这里看起来只是按一下模拟器按钮,实际上验证的是另外两条过滤动作是否已经被系统正常派发给当前接收器。
点击该按钮进行息屏亮屏的模拟器切换:

此时会触发广播接收器回调,并且根据过滤意图,接收到息屏亮屏的广播通知,观察日志:

这里的验证结果说明了两件事:
- 只要动作已经通过
addAction(...)注册到过滤器中,系统发出的对应广播就会进入onReceive(...)。 - 广播接收器并不负责"主动轮询系统状态",它只是被动等待系统分发事件;一旦事件到达,再根据
action分支执行各自的处理逻辑。
3. 静态注册广播接收器及其限制
在清单文件中,静态注册广播接收,通过意图过滤接收对应类型的广播;
但是在 Android 8.0 以上版本,不推荐静态注册广播监听;
3.1 把接收逻辑抽成独立的 BroadcastReceiver 类
如果不希望把接收逻辑直接写成匿名内部类,也可以把它单独抽成一个接收器类。这样做的核心价值不是"代码更短",而是让接收逻辑具备复用空间,后面无论是动态注册还是静态注册,都可以引用同一个类。
先自定义一个广播接收器类:
- 继承
BroadcastReceiver,重写onReceive方法 onReceive()方法内部逻辑是执行在主线程中的,所以在onReceive方法内接收到广播信息后,不要实现耗时操作
java
public class SystemBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "SystemBroadcastReceiver";
@Override
public void onReceive(Context context, Intent intent) {
//这里是主线程操作
//处理接收到的广播信息
String action = intent.getAction();
//电池电量过低
if (action.equals(Intent.ACTION_BATTERY_LOW)) {
Log.i(TAG, "onReceive: 电池电量过低!");
//一般会做对应的UI操作
} else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
Log.i(TAG, "onReceive: 屏幕被关闭!");
} else if (action.equals(Intent.ACTION_SCREEN_ON)) {
Log.i(TAG, "onReceive: 屏幕点亮!");
}
}
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/SystemBroadcastReceiver.java
这里需要特别注意线程语义。onReceive(...) 默认跑在主线程,所以它适合做轻量级分支判断、日志打印、状态同步或者界面提示;如果把网络请求、文件读写这类耗时操作直接塞进去,很容易拖慢主线程,甚至触发卡顿。
3.2 在页面中复用独立接收器
当接收器类已经被抽出来以后,页面里要做的事情反而更清晰了:过滤器负责描述"收什么",接收器实例负责定义"收到后怎么处理",注册调用负责把两者绑定给系统。
在广播接收页面动态注册广播接收器:引入该广播接收器,并结合意图过滤器,一起进行注册
java
//意图过滤器
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_LOW);
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_SCREEN_ON);
//实例一个自定义广播接收器
SystemBroadcastReceiver broadcastReceiver = new SystemBroadcastReceiver();
// 注册广播接收器
registerReceiver(broadcastReceiver, filter);
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/SystemBroadcastActivity.java
这个版本和匿名内部类版本相比,接收流程没有变化,变化的是职责分布:
SystemBroadcastActivity只负责注册时机和生命周期管理。SystemBroadcastReceiver只负责事件分发后的具体处理。IntentFilter继续承担动作过滤职责,不会因为接收器类被抽离而失去作用。
3.3 清单静态注册与 android:exported 的含义
除了在代码里动态注册,还可以把接收器直接写进清单。这样系统在解析应用组件时,就已经知道当前应用对哪些广播感兴趣。
静态添加广播接收器:android:exported="true" 允许 app 外部触发,可以接收到 app 外的相同通知;
xml
<receiver
android:name=".SystemBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BATTERY_LOW" />
<action android:name="android.intent.action.SCREEN_OFF" />
<action android:name="android.intent.action.SCREEN_ON" />
</intent-filter>
</receiver>
BroadcastAndServiceByJavaProject/app/src/main/AndroidManifest.xml
这里的 android:exported 决定的是"外部进程是否可以触发这个组件"。一旦设成 true,说明应用之外的同类广播也可以进入当前接收器;如果只希望应用内部可见,就不能简单放开这个边界。
但是在高版本安卓系统下,静态注册广播接收器不能被触发;
原因并不是接收器类本身有问题,而是系统从 Android 8.0 开始不断收紧后台广播限制。也正因为如此,系统广播监听的主线实现仍然应当优先选择动态注册,把接收能力绑定到明确的页面或服务生命周期上。
4. 监听应用内的自定义广播消息
4.1 登录状态广播要解决的问题
系统广播解决的是"系统状态变化"通知,而业务开发里更常见的是"应用状态变化"同步。登录就是一个典型例子:用户完成登录以后,多个页面可能都需要同时刷新状态,但它们并不适合被登录页逐个持有并直接调用。
当我们登录,或退出登录,整个 app 的特定页面会收到登录状态变化的通知,我们可以自定义广播消息,来实现这个功能;现在我们需要来实现下图这几个页面的跳转,并且在登录页面模拟登录后,其他几个页面都能知道登录状态改变:

这条链路里有三个关键对象:
- 登录页负责在状态变化后发送广播。
- 测试页负责注册接收器并监听登录事件。
Intent负责携带动作名和登录状态参数,把发送端和接收端串起来。
4.2 测试页与登录页的前置界面
入口页面先只负责打开测试页,这样可以先把页面跳转链路建立出来,再把广播同步逻辑加进去。按钮的职责很单一,就是把流程推进到第一个测试页。
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".AutoBroadcastActivity">
<Button
android:id="@+id/btn_test"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="打开几个测试页"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
BroadcastAndServiceByJavaProject/app/src/main/res/layout/activity_auto_broadcast.xml

java
public class AutoBroadcastActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_auto_broadcast);
findViewById(R.id.btn_test).setOnClickListener(view -> {
//启动测试页
startActivity( new Intent(this, AutoTestActivity.class));
});
}
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/AutoBroadcastActivity.java
测试页面的界面结构非常直接:一个 TextView 展示当前登录状态,一个按钮继续打开下一级页面。这样设计的作用是把"页面链路"和"状态广播"放到同一个可观察流程里,后面一旦登录成功,就能同时看日志和界面文本是否同步变化。
xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".AutoTestActivity">
<TextView
android:id="@+id/tv_login_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="用户未登录"
android:textSize="26sp" />
<Button
android:id="@+id/btn_login"
android:layout_centerHorizontal="true"
android:layout_below="@id/tv_login_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="去登录" />
</RelativeLayout>
BroadcastAndServiceByJavaProject/app/src/main/res/layout/activity_auto_test.xml
模拟登录页面只保留一个按钮,目标是把"登录动作发生"这个时刻显式地转换成一条广播事件。
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".LoginActivity">
<Button
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:text="点击我,直接登录!"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
BroadcastAndServiceByJavaProject/app/src/main/res/layout/activity_login.xml
4.3 发送登录状态广播
发送广播的动作本身并不复杂,但每一行代码都承担了明确职责:
- 先创建
Intent,因为广播动作和参数都要挂在它上面。 - 用
setAction(...)指定事件名,这里推荐使用"包名 + 自定义事件名"的形式,避免动作名和其他应用冲突。 - 用
putExtra(...)写入登录状态参数,让接收方不仅知道"有登录事件",还知道"这次事件里的状态值是什么"。 - 最后用
sendBroadcast(...)把事件发出去。
java
public class LoginActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
findViewById(R.id.btn_login).setOnClickListener(view -> {
Intent intent = new Intent();
//添加自定义广播事件名:前面用包名+自定义事件名
intent.setAction("com.ls.broadcastandservicebyjavaproject.loginstatus");
//把已登录的状态放到intent
intent.putExtra("key_login", true);
//发送自定义广播
sendBroadcast(intent, null);
});
}
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/LoginActivity.java
这里的关键不是 sendBroadcast(...) 这个调用本身,而是动作名和参数名必须在发送端与接收端之间保持一致:
- 动作名
com.ls.broadcastandservicebyjavaproject.loginstatus用来判断当前收到的是不是登录相关广播。 - 参数名
key_login用来读取登录结果。 - 参数值
true代表这次发送的是"已登录"状态,后续如果要扩展登出流程,可以继续沿用同一个动作名,只改变布尔值。
4.4 接收登录事件并刷新页面状态
接收侧先要有一个明确的 UI 更新入口,否则广播到了页面以后,很难把"事件分发"和"界面刷新"区分开。这里把文本更新动作封装进 refreshLoginStatus(boolean isLogin),目的是让广播回调只负责取值和分发,不直接掺杂界面拼接细节。
在 AutoTest3Activity 中,接收 LoginActivity 发送的广播事件后,更新 UI:
java
/**
* 登录状态变化后,根据新的状态刷新UI显示
*
* @param isLogin
*/
private void refreshLoginStatus(boolean isLogin) {
tvLoginStatus.setText(TAG + (isLogin ? "\n用户已登录" : "\n用户未登录"));
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/AutoTest3Activity.java
有了 UI 更新方法之后,再把完整接收链路接上:
- 先定义广播接收器,在回调里拿到动作名和登录参数。
- 再构造
IntentFilter,把登录动作加入过滤器。 - 注册时根据系统版本决定是否追加第三个参数。
- Android 8.0 及以上如果使用三参版本
registerReceiver(...),就需要显式声明Context.RECEIVER_EXPORTED或Context.RECEIVER_NOT_EXPORTED,用来说明是否允许外部进程触发这条广播。
接收 LoginActivity 发送的登录事件:
- 定义广播接收器,在广播器回调方法中,获取事件参数,并且判断事件是否是登录相关的事件;
- 设置意图过滤器,将登录事件添加到意图过滤器中;
- 如果接收到的广播事件,会触发广播接收器回调,在回调中判断事件是否是登录事件,如果是则从意图中获取登录状态参数,并调用更新 UI 方法;
- 注册广播接收器,传入广播接收器和意图过滤器;注意,如果是 8.0 以上的安卓系统,需要在注册广播接收器方法中,添加
Context.RECEIVER_EXPORTED参数,表示允许外界访问,等同于在清单文件中,注册广播接收器,需要设置android:exported="true"
java
Log.i(TAG, msg: "onCreate: 页面3已启动");
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals("com.ls.broadcastandservicebyjavaproject.loginstatus")) {
// 拿到登录状态
boolean isLogin = intent.getBooleanExtra(name: "key_login", defaultValue: false);
Log.i(TAG, msg: "onReceive: 接收到登录状态变化" + isLogin);
refreshLoginStatus(isLogin: false);
}
}
};
IntentFilter filter = new IntentFilter();
filter.addAction("com.ls.broadcastandservicebyjavaproject.loginstatus");
// 如果在安卓8.0以上的系统那么需要添加EXPORTED参数 Context.RECEIVER_EXPORTED表示允许其他APP进程触发这个广播事件
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
registerReceiver(broadcast, filter, Context.RECEIVER_EXPORTED);
} else {
registerReceiver(broadcast, filter);
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/AutoTest3Activity.java
如果不想做适配,也可以在项目根目录的 build.gradle 提高 minSdk 版本,然后直接编写 registerReceiver(broadcast, filter, Context.RECEIVER_EXPORTED) 即可;
关闭页面时,需要释放广播资源
java
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(broadcast);
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/AutoTest3Activity.java
这一组代码里有几个必须单独落地的细节:
IntentFilter中只加入登录动作,意味着当前接收器不会误收其他自定义广播。intent.getBooleanExtra(...)的第一个参数是键名,第二个参数是默认值;当前者不存在时,会回退到默认值。Context.RECEIVER_EXPORTED与Context.RECEIVER_NOT_EXPORTED决定的是外部进程能否触发当前接收器,而不是当前页面能否收到本应用内部广播。unregisterReceiver(...)属于释放动作,位置必须和注册动作形成对应关系,否则页面关闭以后仍然保留监听,会造成接收器生命周期悬空。
后续在登录页面设置意图(事件、登录参数)后,在 AutoTest3Activity 中接收广播的事件和登录参数,观察控制台打印日志和页面 UI 变化;
在 AutoTestActivity、AutoTest2Activity 页面中添加上面的流程,然后重新运行重新,观察日志:

这一步的验证重点不是单页刷新,而是多个页面都已经完成接收器注册以后,登录事件能否同时触达多个接收方。只要日志和页面文本都同步变化,就说明"发送端动作名""接收端过滤器""参数读取""UI 更新"这四个环节已经串通。
5. 通过有序广播控制接收顺序
上面的登录状态通知默认属于无序广播,也就是谁先收到、谁后收到没有严格保证。如果业务要求某个页面或模块先处理登录事件,再交给其他接收方继续处理,就要把分发方式切换为有序广播。
5.1 将发送方式切换为 sendOrderedBroadcast
修改发送广播页面的方法为 sendOrderedBroadcast,第二个参数是接收优先级一般不会用到,传 null 即可;
java
findViewById(R.id.btn_login).setOnClickListener(view -> {
Intent intent = new Intent();
//添加自定义广播事件名:前面用包名+自定义事件名
intent.setAction("com.ls.broadcastandservicebyjavaproject.loginstatus");
//把已登录的状态放到intent
intent.putExtra("key_login", true);
//发送自定义无序广播
// sendBroadcast(intent);
//发送有序广播
sendOrderedBroadcast(intent, null);
});
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/LoginActivity.java
这里真正发生变化的是分发语义:
sendBroadcast(...)更像"同时通知所有接收方"。sendOrderedBroadcast(...)更像"把事件按优先级依次传下去"。
发送端只改一行方法调用,但接收端的 priority 设置会因此开始生效。
5.2 使用优先级决定接收先后
优先级参数越大,表示优先级越高,越先接收到广播:
java
//优先级为1
filter.setPriority(1);
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/AutoTest2Activity.java
这条设置的作用非常直接:多个页面都监听同一个动作时,系统会先把广播交给优先级更高的接收器。比如当前工程里:
AutoTest3Activity设为100AutoTestActivity设为30AutoTest2Activity设为1
因此登录状态广播到达时,会先进入页面 3,再进入页面 1,最后进入页面 2。优先级的意义不在于"谁更重要",而在于"谁应该先处理"。
5.3 有序广播适合解决什么问题
有序广播适合用在"多个接收方都关心同一事件,但处理顺序不能随意"的场景里。系统级别的例子是电量、权限、安全策略这类高优先级事件,通常要先交给更关键的服务处理;应用内的例子则更贴近登录、数据同步、页面刷新这样的业务协作。
如果登录成功以后,希望先完成本地状态保存,再触发数据同步,最后才刷新界面,那么有序广播就能把这条顺序显式表达出来。顺序一旦明确,后面的模块协作就不需要再靠额外的回调嵌套维持。
6. 抽离接收器并扩展到跨应用广播
6.1 把登录状态接收逻辑抽成公共接收器
当多个页面监听的是同一条登录状态广播,而且处理逻辑也基本一致时,继续在每个页面里重复写匿名接收器就没有必要了。更合理的做法是把共用逻辑抽成独立类,再通过回调把"登录状态变化"通知给页面自身。
当多个页面,使用的广播接收器的接收事件都是相同的,就可以考虑抽象定义广播接收器的逻辑为一个单独的类:
java
public class LoginStatusBroadcast extends BroadcastReceiver {
private static final String TAG = "LoginStatusBroadcast";
Callback callback;
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals("com.ls.broadcastandservicebyjavaproject.loginstatus")) {
//拿到登录状态
boolean isLogin = intent.getBooleanExtra("key_login", false);
Log.i(TAG, "onReceive: 接收到登录状态变化" + isLogin);
callback.onLogin(isLogin);
}
}
public void setCallback(Callback callback) {
this.callback = callback;
}
public interface Callback {
void onLogin(boolean isLogin);
}
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/LoginStatusBroadcast.java
这个版本把职责拆得非常清楚:
onReceive(...)只负责判断动作名、读取key_login和触发回调。Callback把广播接收层与页面 UI 层隔开。- 页面只要实现
onLogin(boolean isLogin),就能在不关心广播底层细节的情况下刷新界面。
6.2 页面如何接入抽离后的接收器
使用
- 将
broadcast定义为成员变量,表示抽离的广播接收器; - 设置广播接收器回调接口,重写回调方法,用于刷新登录的 UI
java
broadcast = new LoginStatusBroadcast();
broadcast.setCallback(new LoginStatusBroadcast.Callback() {
@Override
public void onLogin(boolean isLogin) {
refreshLoginStatus(isLogin);
}
});
IntentFilter filter = new IntentFilter();
//优先级为100
filter.setPriority(100);
filter.addAction("com.ls.broadcastandservicebyjavaproject.loginstatus");
//如果在安卓8.0以上的系统那么需要添加EXPORTED参数 Context.RECEIVER_EXPORTED表示允许其他APP进程触发这个广播事件
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
registerReceiver(broadcast, filter, Context.RECEIVER_EXPORTED);
} else {
registerReceiver(broadcast, filter);
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/AutoTest3Activity.java
这里的调用顺序不能反过来:
- 先创建
LoginStatusBroadcast实例,否则后面没有对象可注册。 - 再调用
setCallback(...),把页面的 UI 刷新逻辑交给接收器。 - 然后配置过滤器和优先级。
- 最后再调用
registerReceiver(...)。
原因很简单:如果先注册,后设置回调,那么极端情况下广播已经到达,但 callback 还没有准备好,当前页面就没有办法把事件正确转成 UI 刷新动作。
6.3 跨应用发送自定义广播事件
如果广播的目标不再是同一个应用内的多个页面,而是另一个应用进程中的接收器,那么发送端还要额外做一件事:明确指定目标包名。这样系统在分发时就不会把事件投递给其他无关应用。
我们要实现跨进程发送广播事件,在另一个项目中定义意图,对意图设置目标应用的包名、具体的事件和参数数据,并发送
java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
findViewById(R.id.btn_login).setOnClickListener(view -> {
Intent intent = new Intent();
//添加自定义广播事件名:前面用包名+自定义事件名
intent.setAction("com.ls.broadcastandservicebyjavaproject.loginstatus");
// 指定目标应用包名
intent.setPackage("com.1s.broadcastandservicebyjavaproject");
//把已登录的状态放到intent
intent.putExtra("key_login", true);
//发送自定义无序广播
// sendBroadcast(intent);
//发送有序广播
sendOrderedBroadcast(intent, null);
});
}
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/LoginActivity.java
跨进程发送事件的实现,和同进程的区别在于需要在意图指定目标包名;
在接受广播方,区别在于注册广播器的第三个参数,是否允许被外界进程访问,不允许接收跨应用的时间,则传 Context.RECEIVER_NOT_EXPORTED:
java
//如果在安卓8.0以上的系统那么需要添加EXPORTED参数 Context.RECEIVER_NOT_EXPORTED表示不允许其他APP进程触发这个广播事件
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
registerReceiver(broadcast, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
registerReceiver(broadcast, filter);
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/AutoTest2Activity.java
这里需要把 EXPORTED 和 NOT_EXPORTED 的区别彻底看清:
Context.RECEIVER_EXPORTED表示允许外部应用进程触发当前广播接收器。Context.RECEIVER_NOT_EXPORTED表示这条广播只接受应用内部事件,不对外暴露。- 发送端通过
intent.setPackage(...)明确目标包名,接收端再通过导出策略决定要不要接受外部进程的事件,两边同时成立,跨应用广播才能跑通。
在调试两个应用的前提下,日志筛选的包名应该为目标包名,不一定是 mine 了:

完整流程:

到这里,广播的三类常见落地方式已经串起来了:
- 系统广播解决系统状态监听。
- 自定义广播解决应用内多个页面的状态同步。
- 跨应用广播则把事件分发边界扩展到其他进程,但同时也引入了更明确的包名约束和导出控制。
7. 相关代码附录
7.1 系统广播页面与接收器
java
public class SystemBroadcastActivity extends AppCompatActivity {
private static final String TAG = "SystemBroadcastActivity";
private BroadcastReceiver receiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sysytem_broadcast);
findViewById(R.id.btn_regist).setOnClickListener(view -> {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_LOW);
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_SCREEN_ON);
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(Intent.ACTION_BATTERY_LOW)) {
Log.i(TAG, "onReceive: 电池电量过低!");
} else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
Log.i(TAG, "onReceive: 屏幕被关闭!");
} else if (action.equals(Intent.ACTION_SCREEN_ON)) {
Log.i(TAG, "onReceive: 屏幕点亮!");
}
}
};
registerReceiver(receiver, filter);
});
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(receiver);
}
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/SystemBroadcastActivity.java
java
public class SystemBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "SystemBroadcastReceiver";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(Intent.ACTION_BATTERY_LOW)) {
Log.i(TAG, "onReceive: 电池电量过低!");
} else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
Log.i(TAG, "onReceive: 屏幕被关闭!");
} else if (action.equals(Intent.ACTION_SCREEN_ON)) {
Log.i(TAG, "onReceive: 屏幕点亮!");
}
}
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/SystemBroadcastReceiver.java
7.2 自定义广播发送链路
java
public class LoginActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
findViewById(R.id.btn_login).setOnClickListener(view -> {
Intent intent = new Intent();
intent.setAction("com.ls.broadcastandservicebyjavaproject.loginstatus");
intent.putExtra("key_login", true);
sendOrderedBroadcast(intent, null);
});
}
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/LoginActivity.java
java
public class AutoBroadcastActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_auto_broadcast);
findViewById(R.id.btn_test).setOnClickListener(view -> {
startActivity(new Intent(this, AutoTestActivity.class));
});
}
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/AutoBroadcastActivity.java
7.3 登录状态接收器与页面接入
java
public class LoginStatusBroadcast extends BroadcastReceiver {
private static final String TAG = "LoginStatusBroadcast";
Callback callback;
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals("com.ls.broadcastandservicebyjavaproject.loginstatus")) {
boolean isLogin = intent.getBooleanExtra("key_login", false);
Log.i(TAG, "onReceive: 接收到登录状态变化" + isLogin);
callback.onLogin(isLogin);
}
}
public void setCallback(Callback callback) {
this.callback = callback;
}
public interface Callback {
void onLogin(boolean isLogin);
}
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/LoginStatusBroadcast.java
java
public class AutoTest3Activity extends AppCompatActivity {
private static final String TAG = "AutoTest3Activity";
private TextView tvLoginStatus;
private LoginStatusBroadcast broadcast;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_auto_test);
tvLoginStatus = findViewById(R.id.tv_login_status);
findViewById(R.id.btn_login).setOnClickListener(view -> {
startActivity(new Intent(this, LoginActivity.class));
});
refreshLoginStatus(false);
Log.i(TAG, "onCreate: 页面3已启动");
broadcast = new LoginStatusBroadcast();
broadcast.setCallback(new LoginStatusBroadcast.Callback() {
@Override
public void onLogin(boolean isLogin) {
refreshLoginStatus(isLogin);
}
});
IntentFilter filter = new IntentFilter();
filter.setPriority(100);
filter.addAction("com.ls.broadcastandservicebyjavaproject.loginstatus");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
registerReceiver(broadcast, filter, Context.RECEIVER_EXPORTED);
} else {
registerReceiver(broadcast, filter);
}
}
private void refreshLoginStatus(boolean isLogin) {
tvLoginStatus.setText(TAG + (isLogin ? "\n用户已登录" : "\n用户未登录"));
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(broadcast);
}
}
BroadcastAndServiceByJavaProject/app/src/main/java/com/ls/broadcastandservicebyjavaproject/AutoTest3Activity.java
xml
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:usesCleartextTraffic="true"
android:supportsRtl="true"
android:theme="@style/Theme.BroadcastAndServiceByJavaProject"
tools:targetApi="31">
<activity
android:name=".LoginActivity"
android:exported="false" />
<activity
android:name=".AutoTestActivity"
android:exported="false" />
<activity
android:name=".AutoTest2Activity"
android:exported="false" />
<activity
android:name=".AutoTest3Activity"
android:exported="false" />
<activity
android:name=".AutoBroadcastActivity"
android:exported="false" />
<activity
android:name=".SystemBroadcastActivity"
android:exported="false" />
</application>
BroadcastAndServiceByJavaProject/app/src/main/AndroidManifest.xml