广播

如果你了解网络通信原理应该会知道,在一个 IP 网络范围中,最大的 IP 地址是被保留作为广播地址来使用的。

比如某个网络的 IP 范围是 192.168.0.XXX,子网掩码是 255.255.255.0,那么这个网络的广播地址就是 192.168.0.255。广播数据包会被发送到同一网络上的所有端口,这样在该网络中的每台主机都将会收到这条广播。为了便于进行系统级别的消息通知,Android 也引入了一套类似的广播消息机制。

广播机制简介

为什么说 Android 中的广播机制更加灵活呢?这是因为 Android 中的每个应用程序都可以对自己感兴趣的广播进行注册,这样该程序就只会接收到自己所关心的广播内容,这些广播可能是来自于系统的,也可能是来自于其他应用程序的。Android 提供了一套完整的 API,允许应用程序自由地发送和接收广播。发送广播的方法,就是借助 Intent。而接收广播的方法则需要引入一个新的概念 —— 广播接收器(Broadcast Receiver)。

Android 中的广播主要可以分为两种类型:标准广播和有序广播。

标准广播(Normal broadcasts)是一种完全异步执行的广播,在广播发出之后,所有的广播接收器几乎都会在同一时刻接收到这条广播消息,因此它们之间没有任何先后顺序可言。这种广播的效率会比较高,但同时也意味着它是无法被截断的。标准广播的工作流程如图:

有序广播(Ordered broadcasts)是一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完毕后,广播才会继续传递。所以此时的广播接收器是有先后顺序的,优先级高的广播接收器就可以先收到广播消息,并且前面的广播接收器还可以截断正在传递的广播,这样后面的广播接收器就无法收到广播消息了。有序广播的工作流程如图:

接收系统广播

Android 内置了很多系统级别的广播,我们可以在应用程序中通过监听这些广播来得到各种系统的状态信息。比如手机开机完成后会发出一条广播,电池的电量发生变化会发出一条广播,时间或时区发生改变也会发出一条广播,等等。如果想要接收到这些广播,就需要使用广播接收器,下面我们就来看一下它的具体用法。

动态注册监听网络变化

广播接收器可以自由地对自己感兴趣的广播进行注册,这样当有相应的广播发出时,广播接收器就能够收到该广播,并在内部处理相应的逻辑。

注册广播的方式一般有两种,在代码中注册和在 AndroidManifest.xml 中注册,其中前者也被称为动态注册,后者也被称为静态注册。

那么该如何创建一个广播接收器呢?

其实只需要新建一个类,让它继承自 BroadcastReceiver,并重写父类的 onReceive() 方法就行了。这样当有广播到来时,onReceive() 方法就会得到执行,具体的逻辑就可以在这个方法中处理。

那我们就先通过动态注册的方式编写一个能够监听网络变化的程序,借此学习一下广播接收器的基本用法吧。

新建一个 BroadcastTest 项目,然后修改 MainActivity 中的代码,如下所示:

package com.example.broadcasttest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private IntentFilter intentFilter;
private NetWorkChangeReceiver netWorkChangeReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
netWorkChangeReceiver = new NetWorkChangeReceiver();
registerReceiver(netWorkChangeReceiver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(netWorkChangeReceiver);
}
class NetWorkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "network changes", Toast.LENGTH_SHORT).show();
}
}
}

可以看到,我们在 MainActivity 中定义了一个内部类 NetworkChangeReceiver。这个类是继承自 BroadcastReceiver 的,并重写了父类的 onReceive() 方法。这样每当网络状态发生变化时,onReceive() 方法就会得到执行,这里只是简单地使用 Toast 提示了一段文本信息。

然后观察 onCreate() 方法,首先创建了一个 IntentFilter 的实例,并给它添加了一个值为 android.net.conn.CONNECTIVITY_CHANGE 的 action。为什么要添加这个值呢?因为当网络状态发生变化时,系统发出的正是一条值为 android.net.conn.CONNECTIVITY_CHANGE 的广播,也就是说我们的广播接收器想要监听什么广播,就在这里添加相应的 action。

接下来创建了一个 NetworkChangeReceiver 的实例,然后调用 registerReceiver() 方法进行注册,将 NetworkChangeReceiver 的实例和 IntentFilter 的实例都传了进去。这样 NetworkChangeReceiver 就会收到所有值为 android.net.conn.CONNECTIVITY_CHANGE 的广播,也就实现了监听网络变化的功能。

最后要记得,动态注册的广播接收器一定都要取消注册才行。这里我们是在 onDestroy() 方法中通过调用 unregisterReceiver() 方法来实现的。

整体来说,代码还是非常简单的,现在运行一下程序。首先你会在注册完成的时候收到一条广播, 然后按下 Home 键回到主界面 (注意不能按 Back 键, 否则 onDestroy() 方法会执行), 接着打开 Settings 程序 → Data usage 进入到数据使用详情界面, 然后尝试着开关 Cellular data 按钮来启动和禁用网络, 你就会看到有 Toast 提醒你网络发生了变化。

首先你会在注册完成的时候收到一条广播,图示如下:

然后按下 Home 键回到主界面 (注意不能按 Back 键, 否则 onDestroy() 方法会执行), 接着打开 Settings 程序 → Data usage 进入到数据使用详情界面, 然后尝试着开关 Cellular data 按钮来启动和禁用网络, 你就会看到有 Toast 提醒你网络发生了变化。图示如下:

具体的操作和书中所说有所差别,但是效果一样。

不过,只是提醒网络发生了变化还不够人性化,最好是能准确地告诉用户当前是有网络还是没有网络,因此我们还需要对上面的代码进行进一步的优化。修改 MainActivity 中的代码,如下所示:

package com.example.broadcasttest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private IntentFilter intentFilter;
private NetWorkChangeReceiver netWorkChangeReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
netWorkChangeReceiver = new NetWorkChangeReceiver();
registerReceiver(netWorkChangeReceiver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(netWorkChangeReceiver);
}
class NetWorkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
// 下面一行会爆红,点击红色小灯泡第一行就行。意思是在 AndroidManifest.xml 文件中添加:
// <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isAvailable()) {
Toast.makeText(context, "network is available", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "network is unavailable", Toast.LENGTH_SHORT).show();
}
}
}
}

onReceive() 方法中,首先通过 getSystemService() 方法得到了 ConnectivityManager 的实例,这是一个系统服务类,专门用于管理网络连接的。然后调用它的 getActiveNetworkInfo() 方法可以得到 NetworkInfo 的实例,接着调用 NetworkInfo 的 isAvailable() 方法,就可以判断出当前是否有网络了,最后我们还是通过 Toast 的方式对用户进行提示。

另外,这里有非常重要的一点需要说明,Android 系统为了保护用户设备的安全和隐私,做了严格的规定:如果程序需要进行一些对用户来说比较敏感的操作,就必须在配置文件中声明权限才可以,否则程序将会直接崩溃。比如这里访问系统的网络状态就是需要声明权限的。打开 AndroidManifest.xml 文件,在里面加入如下权限就可以访问系统网络状态了:(就是上面代码中爆红的位置的说明)

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

其实 Android 中有许多操作都是需要声明权限才可以进行的,后面我们还会不断使用新的权限。

不过目前这个访问系统网络状态的权限还是比较简单的,只需要在 AndroidManifest.xml 文件中声明一下就可以了,而 Android 6.0 系统中引入了更加严格的运行时权限,从而能够更好地保证用户设备的安全和隐私,关于这部分内容我们将在第 7 章中学习。

这里飞行模式也会有影响,原因未知。

现在重新运行程序,然后按下 Home 键→ Settings→Data usage,进入到数据使用详情界面,关闭 Cellular data 会弹出无网络可用的提示,如图:

不打开飞行模式:

打开飞行模式:

然后重新打开 Cellular data 又会弹出网络可用的提示。

静态注册实现开机启动

动态注册的广播接收器可以自由地控制注册与注销,在灵活性方面有很大的优势,但是它也存在着一个缺点,即必须要在程序启动之后才能接收到广播,因为注册的逻辑是写在 onCreate() 方法中的。

那么有没有什么办法可以让程序在未启动的情况下就能接收到广播呢?这就需要使用静态注册的方式了。

这里我们准备让程序接收一条开机广播,当收到这条广播时就可以在 onReceive() 方法里执行相应的逻辑,从而实现开机启动的功能。

可以使用 Android Studio 提供的快捷方式来创建一个广播接收器,右击 com.zhouzhou.broadcasttest 包 → New → Other → BroadcastReceiver,会弹出如图所示的窗口:

可以看到,这里将广播接收器命名为 BootCompleteReceiver, Exported 属性表示是否允许这个广播接收器接收本程序以外的广播,Enabled 属性表示是否启用这个广播接收器。勾选这两个属性,点击 Finish 完成创建。

然后修改 BootCompleteReceiver 中的代码,如下所示:

package com.example.broadcasttest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class BootCompleteReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Boot Complete", Toast.LENGTH_LONG).show();
}
}

代码非常简单, 我们只是在 onReceive() 方法中使用 Toast 弹出一段提示信息。另外,静态的广播接收器一定要在 AndroidManifest.xml 文件中注册才可以使用,不过由于我们是使用 Android Studio 的快捷方式创建的广播接收器,因此注册这一步已经被自动完成了。打开 AndroidManifest.xml 文件瞧一瞧,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BroadcastTest">
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true"></receiver>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

可以看到,<application> 标签内出现了一个新的标签 <receiver>,所有静态的广播接收器都是在这里进行注册的。它的用法其实和 <activity> 标签非常相似,也是通过 android:name 来指定具体注册哪一个广播接收器,而 enabled 和 exported 属性则是根据我们刚才勾选的状态自动生成的。

不过目前 BootCompleteReceiver 还是不能接收到开机广播的,我们还需要对 AndroidManifest. xml 文件进行修改才行,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BroadcastTest">
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

由于 Android 系统启动完成后会发出一条值为 android.intent.action.BOOT_COMPLETED 的广播, 因此我们在 <intent-filter> 标签里添加了相应的的 action。另外, 监听系统开机广播也是需要声明权限的, 可以看到, 我们使用 <uses-permissionm> 标签又加入了一条 androidpermission.RECEIVE_BOOT_COMPLETED 权限。

现在重新运行程序后, 我们的程序就已经可以接收开机广播了。将模拟器关闭并重新启动, 在启动完成之后就会收到开机广播, 如图:

具体操作是:(手机)长按关机键,唤出关机按钮,关机。然后再重新启动模拟器,启动之后,马上按 Home 键,就是得赶上没有完全开机之前,进入到桌面。然后就是等待完全开机,就会跳出 Toast:“Boot Complete”。

到目前为止,在广播接收器的 onReceive() 方法中都只是简单地使用 Toast 提示了一段文本信息,当你真正在项目中使用到它的时候,就可以在里面编写自己的逻辑。

需要注意的是,不要在 onReceive() 方法中添加过多的逻辑或者进行任何的耗时操作,因为在广播接收器中是不允许开启线程的,当 onReceive() 方法运行了较长时间而没有结束时,程序就会报错。因此广播接收器更多的是扮演一种打开程序其他组件的角色,比如创建一条状态栏通知,或者启动一个服务等,这几个概念我们会在后面的章节中学到。

发送自定义广播

广播主要分为两种类型:标准广播和有序广播,在本节中我们就将通过实践的方式来看一下这两种广播具体的区别。

发送标准广播

在发送广播之前,还是需要先定义一个广播接收器来准备接收此广播才行,不然发出去也是白发。因此新建一个 MyBroadcastReceiver。

MyBroadcastReceiver 的代码如下所示:

package com.example.broadcasttest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received in MyBroadcastReceiver", Toast.LENGTH_SHORT).show();
}
}

这里当 MyBroadcastReceiver 收到自定义的广播时,就会弹出 “received in MyBroadcastReceiver” 的提示。然后在 AndroidManifest.xml 中对这个广播接收器进行修改:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BroadcastTest">
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

这里让 MyBroadcastReceiver 接收一条值为 com.example.broadcasttest.MY_BROADCAST 的广播,因此待会儿在发送广播的时候,我们就需要发出这样的一条广播。接下来修改 activity_main.xml 中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send Broadcast" />
</LinearLayout>

这里在布局文件中定义了一个按钮,用于作为发送广播的触发点。然后修改 MainActivity 中的代码,如下所示:

package com.example.broadcasttest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private IntentFilter intentFilter;
private NetWorkChangeReceiver netWorkChangeReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent("com.zhouzhou.broadcasttest.MY_BROADCAST");
// 书中缺少:intent.setComponent("广播接收器得包的路径名","广播接收器的类路径名")
intent.setComponent(new ComponentName("com.zhouzhou.broadcasttest", "com.zhouzhou.broadcasttest.MyBroadcastReceiver"));
sendBroadcast(intent);
}
});
intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
netWorkChangeReceiver = new NetWorkChangeReceiver();
registerReceiver(netWorkChangeReceiver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(netWorkChangeReceiver);
}
class NetWorkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isAvailable()) {
Toast.makeText(context, "network is available", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "network is unavailable", Toast.LENGTH_SHORT).show();
}
}
}
}

可以看到,在按钮的点击事件里面加入了发送自定义广播的逻辑。首先构建出了一个 Intent 对象,并把要发送的广播的值传入,然后调用了 Context 的 sendBroadcast() 方法将广播发送出去,这样所有监听 com.example.broadcasttest.MY_BROADCAST 这条广播的广播接收器就会收到消息。此时发出去的广播就是一条标准广播。

重新运行程序,并点击一下 Send Broadcast 按钮,效果如图:

这样就成功完成了发送自定义广播的功能。另外,由于广播是使用 Intent 进行传递的,因此还可以在 Intent 中携带一些数据传递给广播接收器。

发送有序广播

广播是一种可以跨进程的通信方式,这一点从前面接收系统广播的时候就可以看出来了。因此在我们应用程序内发出的广播,其他的应用程序应该也是可以收到的。

为了验证这一点,我们需要再新建一个 BroadcastTest2 项目,点击 Android Studio 导航栏 → File → New → New Project 进行创建。将项目创建好之后,还需要在这个项目下定义一个广播接收器,用于接收上一小节中的自定义广播。新建 AnotherBroadcastReceiver,代码如下所示:

package com.example.broadcasttest2;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class AnotherBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received in AnotherBroadcastReceiver", Toast.LENGTH_SHORT).show();
}
}

这里仍然是在广播接收器的 onReceive() 方法中弹出了一段文本信息。然后在 AndroidManifest.xml 中对这个广播接收器进行修改,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest2">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BroadcastTest2">
<receiver
android:name=".AnotherBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<!-- 同样接收的是com.example.broadcasttest.MY_BROADCAST这条广播 -->
<action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

可以看到,AnotherBroadcastReceiver 同样接收的是 com.example.broadcasttest.MY_BROADCAST 这条广播。现在运行 BroadcastTest2 项目将这个程序安装到模拟器上:

然后重新回到 BroadcastTest 项目的主界面,并点击一下 Send Broadcast 按钮,就会分别弹出两次提示信息,如图所示:

没能实现效果,待研究。

这样就强有力地证明了,我们的应用程序发出的广播是可以被其他的应用程序接收到的。到目前为止,程序里发出的都还是标准广播,现在尝试一下发送有序广播。

重新回到 BroadcastTest 项目,然后修改 MainActivity 中的代码,如下所示:

package com.example.broadcasttest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private IntentFilter intentFilter;
private NetWorkChangeReceiver netWorkChangeReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
// 书中缺少:intent.setComponent("广播接收器得包的路径名","广播接收器的类路径名")
intent.setComponent(new ComponentName("com.example.broadcasttest", "com.example.broadcasttest.MyBroadcastReceiver"));
// sendBroadcast(intent);
sendOrderedBroadcast(intent, null);
}
});
intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
netWorkChangeReceiver = new NetWorkChangeReceiver();
registerReceiver(netWorkChangeReceiver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(netWorkChangeReceiver);
}
class NetWorkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isAvailable()) {
Toast.makeText(context, "network is available", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "network is unavailable", Toast.LENGTH_SHORT).show();
}
}
}
}

可以看到,发送有序广播只需要改动一行代码,即将 sendBroadcast() 方法改成 sendOrderedBroadcast() 方法。sendOrderedBroadcast() 方法接收两个参数,第一个参数仍然是 intent,第二个参数是一个与权限相关的字符串,这里传入 null 就行了。

现在重新运行程序,并点击 Send Broadcast 按钮,发现两个应用程序仍然都可以接收到这条广播。看上去好像和标准广播没什么区别嘛,不过别忘了,这个时候的广播接收器是有先后顺序的,而且前面的广播接收器还可以将广播截断,以阻止其继续传播。

那么该如何设定广播接收器的先后顺序呢?当然是在注册的时候进行设定的了,修改 AndroidManifest.xml 中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BroadcastTest">
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter android:priority="100">
<action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

通过 android:priority 属性给广播接收器设置了优先级,优先级比较高的广播接收器就可以先收到广播。这里将 MyBroadcastReceiver 的优先级设成了 100,以保证它一定会在 AnotherBroadcastReceiver 之前收到广播。

既然已经获得了接收广播的优先权,那么 MyBroadcastReceiver 就可以选择是否允许广播继续传递了。修改 MyBroadcastReceiver 中的代码,如下所示:

package com.example.broadcasttest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received in MyBroadcastReceiver", Toast.LENGTH_SHORT).show();
abortBroadcast();
}
}

书中讲述:“如果在 onReceive() 方法中调用了 abortBroadcast() 方法,就表示将这条广播截断,后面的广播接收器将无法再接收到这条广播。现在重新运行程序,并点击一下 Send Broadcast 按钮,你会发现,只有 MyBroadcastReceiver 中的 Toast 信息能够弹出,说明这条广播经过 MyBroadcastReceiver 之后确实是终止传递了。” 实际上,在 4.4 以上,abortBroadcast() 方法不能实现拦截功能了。上面的测试,并没有成功拦截。

使用本地广播

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

这样就很容易引起安全性的问题,比如说我们发送的一些携带关键性数据的广播有可能被其他的应用程序截获,或者其他的程序不停地向我们的广播接收器里发送各种垃圾广播。

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

这样就很容易引起安全性的问题,比如说我们发送的一些携带关键性数据的广播有可能被其他的应用程序截获,或者其他的程序不停地向我们的广播接收器里发送各种垃圾广播。

为了能够简单地解决广播的安全性问题,Android 引入了一套本地广播机制,使用这个机制发出的广播只能够在应用程序的内部进行传递,并且广播接收器也只能接收来自本应用程序发出的广播,这样所有的安全性问题就都不存在了。

本地广播的用法并不复杂,主要就是使用了一个 LocalBroadcastManager 来对广播进行管理,并提供了发送广播和注册广播接收器的方法。下面我们就通过具体的实例来尝试一下它的用法,修改 MainActivity 中的代码,如下所示:

package com.example.broadcasttest;
import androidx.appcompat.app.AppCompatActivity;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private IntentFilter intentFilter;
private LocalReceiver localReceiver;
private LocalBroadcastManager localBroadcastManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
localBroadcastManager = LocalBroadcastManager.getInstance(this); // 获取实例
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(intent); // 发送本地广播
}
});
intentFilter = new IntentFilter();
intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");
localReceiver = new LocalReceiver();
localBroadcastManager.registerReceiver(localReceiver, intentFilter); // 注册本地广播监听器
}
@Override
protected void onDestroy() {
super.onDestroy();
localBroadcastManager.unregisterReceiver(localReceiver);
}
class LocalReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received local broadcast", Toast.LENGTH_SHORT).show();
}
}
}

有没有感觉这些代码很熟悉? 没错, 其实这基本上就和前面所学的动态注册广播接收器以及发送广播的代码是一样的。只不过现在首先是通过 LocalBroadcastManagergetInstance() 方法得到了它的一个实例,然后在注册广播接收器的时候调用的是 LocalBroadcastManagerregisterReceiver() 方法,在发送广播的时候调用的是 LocalBroadcastManagersendBroadcast() 方法,仅此而已。这里我们在按钮的点击事件里面发出了一条 com.example.broadcasttest.LOCAL_BROADCAST 广播,然后在 LocalReceiver 里去接收这条广播。重新运行程序,并点击 Send Broadcast 按钮,效果如图:

可以看到,LocalReceiver 成功接收到了这条本地广播,并通过 Toast 提示了出来。如果你还有兴趣进行实验,可以尝试在 BroadcastTest2 中也去接收 com.example.broadcasttest.LOCAL_BROADCAST 这条广播。答案是显而易见的,肯定无法收到,因为这条广播只会在 BroadcastTest 程序内传播。(经过测试,竟然也能收到!!!)

另外还有一点需要说明,本地广播是无法通过静态注册的方式来接收的。其实这也完全可以理解,因为静态注册主要就是为了让程序在未启动的情况下也能收到广播,而发送本地广播时,我们的程序肯定是已经启动了,因此也完全不需要使用静态注册的功能。

盘点一下使用本地广播的几点优势:

❑ 可以明确地知道正在发送的广播不会离开我们的程序,因此不必担心机密数据泄漏。

❑ 其他的程序无法将广播发送到我们程序的内部,因此不需要担心会有安全漏洞的隐患。

❑ 发送本地广播比发送系统全局广播将会更加高效。

广播的最佳实践 —— 实现强制下线功能

我现在我们就准备通过一个完整例子的实践, 来综合运用一下本章中所学到的知识。

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

Git 时间 —— 初识版本控制工具

posted @   有空  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
点击右上角即可分享
微信分享提示

目录导航