Android开发学习笔记(六) —— 重温BroadcastReceiver

一、什么是BroadcastReceiver

广播(Broadcast)是一种广泛运用的在应用程序之间传输信息的机制。而广播接收器(BroadcastReceiver)是Android四大组件之一,用于对发送出来的广播进行过滤接受并响应的一类组件。它允许应用程序在某些事件发生时作出响应,无论应用是否在前台运行。它的工作原理是基于发布-订阅模式。应用程序可以选择注册感兴趣的广播消息,并在消息到达时进行响应。这种机制使得不同的应用程序之间能够实现通信和协作,同时也提供了一种系统级别的事件通知机制。广播接收器在 Android 开发中非常有用,可以用于实现很多功能,如接收来电或短信提醒、处理网络状态变化、监听系统事件等。

1.1 标准广播和有序广播

  1. 标准广播

    完全异步执行的广播,所有的接收者几乎同时接收到广播消息。这种广播不保证接收者能够同时处理广播,也不能保证接收者按照特定的顺序接收到广播。

  2. 有序广播

    同步执行的一种广播,发出广播后,同一时间只有一个广播接收者能收到,当这个广播接收者的逻辑执行完后,广播才会继续传递。当然,前面的接受者还可以截断广播的继续传递,那么后续接受者就无法收到广播信息了。

静态注册和动态注册

不同于其他三大组件,广播接收器既可以在AndroidManifest.xml中进行静态注册,也可以在代码中进行动态注册。

  1. 静态注册

    AndroidManifest.xml文件中进行注册。

    静态注册的广播接收器与设备共存亡,可以让应用在未运行时也能接收到广播消息。

  2. 动态注册

    在代码中使用registerReceiver()进行注册,并在适当的时候调用 unregisterReceiver() 方法进行解注册。

    动态注册的广播接收器与注册它的Activity共存亡,只有当应用处于运行状态时,接收器才会接收到广播消息。

二、BroadcastReceiver的基本使用

2.1 静态注册接收开机广播

首先创建一个BootCompleteReceiver类,重写onReceive()方法,进行简单的弹窗提示。

1
2
3
4
5
6
7
8
public class BootCompleteReceiver extends BroadcastReceiver {
private final String ACTION_BOOT = "android.intent.action.BOOT_COMPLETED";
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_BOOT.equals(intent.getAction()))
Toast.makeText(context, "开机完毕~", Toast.LENGTH_LONG).show();
}
}

因为是静态注册,所以需要在AndroidManifest.xml文件中进行注册

1
2
3
4
5
<receiver android:name=".BootCompleteReceiver">
<intent-filter>
<action android:name = "android.intent.cation.BOOT_COMPLETED">
</intent-filter>
</receiver>

当设备启动完成后,广播接收器就会收到名为 android.intent.action.BOOT_COMPLETED 的广播,并执行相应的逻辑操作。

除此之外,还要给应用添加权限。

1
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

主活动中不需要添加任何代码。

2.2 动态注册监听网络状态变化

以监听网络变化为例,讲解如何动态注册广播接收者。

首先我们需要创建一个BroadcastReceiver类,重写父类的onReceive()方法来实现网络变化后动作(比如弹窗提示啥的)。

1
2
3
4
5
6
public class NetworkReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "网络状态发生改变", Toast.LENGTH_SHORT).show();
}
}

然后再主活动中动态注册该广播接收者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MainActivity extends AppCompatActivity {

private NetworkReceiver networkReceiver;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//实例化广播接收者
networkReceiver = new NetworkReceiver();
//添加意图为网络状态改变
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
//使用registerReceiver()动态注册广播接收者
registerReceiver(networkReceiver, intentFilter);
}

@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(networkReceiver);
}
}

当网络状态发生变化时,系统会发出一条值为android.net.conn.CONNECTIVITY_CHANGE的广播,因为我们的广播接收器需要监听这个广播,所以action就设置此值。

最后,不要忘记将广播接收器取消注册。动态注册的广播接收器与活动共存亡,所以重写活动的onDestroy()方法,调用unregisterReceiver()方法来实现。

2.3 发送自定义广播

上面两个案例都是讲如何接收广播,接下来我们来学习如何发送广播。在前面已经介绍过,广播主要分为两种类型:标准广播和有序广播。那么这两种广播如何发送呢?

2.3.1 发送标准广播

在发送广播前,我们仍然要定义一个广播接收器来接收我们发送的广播。

1
2
3
4
5
6
7
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("MyBroadcastReceiver", "Received");
Toast.makeText(context, "MyBroadcastReceiver received", Toast.LENGTH_SHORT).show();
}
}

AndroidManifest.xml中进行注册。

1
2
3
4
5
6
7
8
<receiver android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<!--自定义一个动作-->
<action android:name="com.example.broadcasttest.MY_BROADCAST"/>
</intent-filter>
</receiver>

在主活动的xml中定义一个按钮,用来发送广播。最后是MainActivity.java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MainActivity extends AppCompatActivity {

private NetworkReceiver networkReceiver;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

public void sendBroadcast(View view) {
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
//SDK版本 > 26需要添加
//intent.addFlags(0x01000000);
sendBroadcast(intent);
}
}

原本到这里就可以实现功能了。但是我这里出了点小插曲。点击按钮后,没有反应,而且Logcat中提示:

1
Background execution not allowed: receiving Intent { act=com.example.broadcasttest.MY_BROADCAST flg=0x2010 } to com.example.testbroadcastreceiver/.MyBroadcastReceiver

原因是自 Android 8.0(API 级别 26)起,Android 引入了后台执行限制,以提高设备性能和安全性。这意味着在后台运行的应用程序将无法接收到大多数隐式广播,除非应用程序处于前台或具有前台服务。

解决方法:

  1. 仍然使用静态注册广播,给Intent添加FLAG_RECEIVER_INCLUDE_BACKGROUND的Flag,不过这个标志位在源码中被hide掉了,但是可以直接使用这个属性值。

    1
    intent.addFlags(0x01000000);
  2. 仍然使用静态注册广播,但将原来的隐式Intent变成显式Intent,即指定包名。

    1
    2
    Intent intent = new Intent(this,MyBroadcastReceiver.class);
    sendBroadcast(intent);
  3. 仍然使用静态注册广播,但将广播接收器改为前台服务。

  4. 降低SDK版本。

  5. AndroidManifest.xml中删除注册,使用动态注册广播接收器,发送广播的代码不变。

2.3.2 发送有序广播

另外创建一个项目,同样接收com.example.broadcasttest.MY_BROADCAST的广播消息,同样弹窗显示:

1
2
3
4
5
6
public class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "另一个应用的静态广播接收器收到该广播", Toast.LENGTH_SHORT).show();
}
}

记得在AndroidManifest.xml中注册:

1
2
3
4
5
6
<receiver android:name="com.example.testanotherbroadcastreceiver.MyReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCAST"/>
</intent-filter>
</receiver>

然后将其安装到测试机上。

发送有序广播的代码在上 2.3.1 的基础上,只需要将sendBroadcast(intnet)方法改为sendOrderedBroadcast(intent,receiverPermission )方法即可。其中参数receiverPermission 是一个与权限有关的字符串,这里传入 null 就行了。

点击发送按钮可以看到依次出现两个弹窗。

a. 设置广播接收器的优先级

既然有序广播在接收时是有先后顺序的,那么就我们可以设置广播接收器的先后顺序。在AndroidManifest.xml<intent-filter>中,使用属性android:priority来设置优先级,值越大优先级越高,优先级越高的广播接收器就越先接收到广播。

b. 截断广播传递

既然已经获得了接收广播的优先权,那么我们就可以对这条广播进行截断了。操作起来比较容易,只需要在自定义的BroadcastReceiver类的onReceive()方法中添加abortBroadcast()方法即可,相关代码如下:

1
2
3
4
5
6
7
8
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("MyBroadcastReceiver", "Received");
Toast.makeText(context, "MyBroadcastReceiver received", Toast.LENGTH_SHORT).show();
abortBroadcast();
}
}

2.4 使用本地广播

在前面的代码中,我们发送和接收的广播全部属于系统全局广播,即发出的广播可以被其他任何应用程序接收到,并且我们也可以接收来自于其他任何应用程序的广播。

但这样就很容易引起安全性的问题,比如说我们发送的一些携带关键性数据的广播有可能被其他的应用程序截获,或者其他的程序不停地向我们的广播接收器里发送各种垃圾广播。因此Android引入了一套本地广播机制来解决这些安全性问题,使用这个机制发出的广播只能够在应用程序的内部进行传递,并且广播接收器也只能接收来自本应用程序发出的广播。

本地广播的用法并不复杂,主要就是使用了一个LocalBroadcastManager来对广播进行管理,并提供了发送广播和注册广播接收器的方法。

在使用前,我们需要注意一些事项:

  1. 本地广播无法通过静态注册来接收。因为发送本地广播时,程序肯定已经启动了,没有必要用静态注册。况且静态注册的话,会接收到来自非自生应用程序的广播。
  2. 在广播中启动Activity的话,需要Intent中添加FLAG_ACTIVITY_NEW_TASK的Flag,不然会报错,因为需要一个栈来存放新打开的Activity。
  3. 如果通过广播来弹出AlertDialog,需要设置对话框的类型为TYPE_SYSTEM_ALERT

下面我们就通过具体的实例来尝试一下它的用法。首先自定义的BroadcastReceiver类,代码同上:

1
2
3
4
5
6
7
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("MyBroadcastReceiver", "Received");
Toast.makeText(context, "MyBroadcastReceiver received", Toast.LENGTH_SHORT).show();
}
}

最关键的是MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class MainActivity extends AppCompatActivity {

private static final String ACTION_MY_BROADCAST = "com.example.broadcasttest.MY_BROADCAST";
private LocalBroadcastManager localBroadcastManager;
private MyBroadcastReceiver MyBroadcastReceiver;


@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//获取实例
localBroadcastManager = LocalBroadcastManager.getInstance(this);
//注册本地广播接收器
IntentFilter intentFilter = new IntentFilter(ACTION_MY_BROADCAST);
MyBroadcastReceiver = new MyBroadcastReceiver();
localBroadcastManager.registerReceiver(MyBroadcastReceiver, intentFilter);

}

public void sendBroadcast(View view) {
Intent intent = new Intent(ACTION_MY_BROADCAST);
//intent.addFlags(0x01000000);
//发送本地广播
localBroadcastManager.sendBroadcast(intent);
}

@Override
protected void onDestroy() {
super.onDestroy();
//记得要取消注册
localBroadcastManager.unregisterReceiver(MyBroadcastReceiver);
}
}

跟动态注册的代码没有很大的区别,只不过是通过localBroadcastManager来注册和销毁广播接收器。

三、广播的最佳实战 —— 实现强制下线功能

强制下线功能应该算是比较常见的了,很多的应用程序都具备这个功能,比如你的QQ号在别处登录了,就会将你强制挤下线。其实实现强制下线功能的思路也比较简单,只需要在界面上弹出一个对话框,让用户无法进行任何其他操作,必须要点击对话框中的确定按钮,然后回到登录界面即可。可是这样就存在着一个问题,因为当我们被通知需要强制下线时可能正处于任何一个界面,难道需要在每个界面上都编写一个弹出对话框的逻辑?其实并不这么做,我们完全可以借助本章中所学的广播知识,来非常轻松地实现这一功能。

强制下线功能需要先关闭掉所有的活动,然后回到登录界面。因此我们可以利用在Activity章节学到的创建一个活动管理类。

首先创建一个ActivityCollector类用于管理所有的活动,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ActivityCollector {

public static List<Activity> activityList = new ArrayList<>();

public static void addActivity(Activity activity){
activityList.add(activity);
}

public static void removeActivity(Activity activity){
activityList.remove(activity);
}

public static void finishAll(){

for (Activity each:activityList) {
if (!each.isFinishing()){
each.finish();
}
}
activityList.clear();
}

}

然后创建BaseActivity类作为所有活动的父类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BaseActivity extends AppCompatActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityCollector.addActivity(this);
}

@Override
protected void onDestroy() {
super.onDestroy();
ActivityCollector.removeActivity(this);
}

}

接着开始实现我们所需要的功能,首先是创建一个登陆界面的活动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class loginActivity extends BaseActivity{

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
}

public void ClicktoLogin(View view) {
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
}

}

极致的简陋,就一个登入按钮和它的点击事件,账号密码都不想要了(对于这个案例来讲并不重要)。

记得注册这个活动,并将其设置为主活动而不是MainActivity。

然后是强制下线界面的活动,这里写在MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends BaseActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

public void ClicktoFinishAllActivities(View view) {
//自定义action,随便就行
Intent intent = new Intent("LOGIN_OTHER");
sendBroadcast(intent);
}

}

同样还是一个按钮和它的点击事件,用于强制下线。随便定义一个广播动作即可,只要与广播接收器的动作一致就行。

现在功能都写好了,接下来我们就需要创建一个广播接收器来接收这条强制下线广播,唯一的问题就是,应该在哪里创建呢?

首先不可能时静态注册,因为静态注册没有办法在onReceive()方法里弹出对话框这样的UI控件的。其次,我们不可能在每个具体活动中都注册一个动态广播接收器,这显然不合理。但是我们写的每个活动都必须保证能够相应广播,也就是有广播接收器。那么我们将注册广播接收器的代码写到他们的父类BaseActivity中不就行了?这样一来确确实实每个活动都有广播接收器了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class BaseActivity extends AppCompatActivity {

public ForceOffLineReceiver forceOffLineReceiver;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityCollector.addActivity(this);
}

@Override
protected void onResume() {
super.onResume();
//注册广播接收器
forceOffLineReceiver = new ForceOffLineReceiver();
IntentFilter intentFilter = new IntentFilter("LOGIN_OTHER");
registerReceiver(forceOffLineReceiver,intentFilter);
}

@Override
protected void onPause() {
super.onPause();
if (forceOffLineReceiver != null){
unregisterReceiver(forceOffLineReceiver);
forceOffLineReceiver = null;
}
}

@Override
protected void onDestroy() {
super.onDestroy();
ActivityCollector.removeActivity(this);
}

class ForceOffLineReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("aaa", "onReceive: ");
//实现弹窗警告
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context);
dialogBuilder.setTitle("警告:")
.setMessage("您的账号在别处登录,请重新登陆")
.setCancelable(false)
.setPositiveButton("确认", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
Log.d("aaa", "onClick: ");
//关闭所有活动
ActivityCollector.finishAll();
//返回登录界面
Intent backintent = new Intent(context, loginActivity.class);
backintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(backintent);
}
});
dialogBuilder.show();
//AlertDialog alertDialog = dialogBuilder.create();
//alertDialog.show();
}
}

}

我们额外重写了onResume()onPause()这两个生命周期函数,然后分别在这两个方法里注册和取消注册了ForceOfflineReceiver.那么为什么要这样写呢?

因为我们只需要保证应用的Activity栈中栈顶活动能够接收这条强制下线广播就足够了,非栈顶活动没有必要去接收这条广播,所以写在onResume()onPause()方法里就可以很好地解决这个问题,当一个活动失去栈顶位置时就会自动取消广播接收器的注册。

注意事项:

不能使用localBroadcastManager来注册广播,不然程序异常退出!!!


Android开发学习笔记(六) —— 重温BroadcastReceiver
http://example.com/2023/08/12/Android安全/Android开发学习笔记(六) —— 重温BroadcastReceiver/
作者
gla2xy
发布于
2023年8月12日
许可协议