为了方便于进行系统级别的消息通知, Android 引入了一套类似的广播消息机制,而且 Android 中的广播机制显得很灵活。它是 Android 四大组件之一,主要用于接收系统或者 App 发送的广播事件。与广播配套是用的是广播接收器,它是一种用于响应系统范围广播通知的组件。许多广播都是由系统发起的,当然应用也可以发起广播。尽管广播接收器不会显示用户界面,但它们可以创建状态栏通知,在发生广播事件时提醒用户。但广播接收器更常见的用途只是作为通向其他组件的“通道”,设计用于执行极少量的工作。 例如,它可能会基于事件发起一项服务来执行某项工作。本篇就将对这一机制的方方面面进行详细的讲解。
本节例程下载地址:WillFlowBroadcast
一、广播机制简介
1、广播机制概述
为什么说 Android 中的广播机制更加灵活呢?这是因为 Android 中的每个应用程序都可以对自己感兴趣的广播进行注册,这样该程序就只会接收到自己所关心的广播内容,这些广播可能是来自于系统的,例如,通知屏幕已关闭、电池电量不足或已拍摄照片的广播。也可能是来自于其他应用程序的,例如,通知其他应用某些数据已下载至设备,并且可供其使用。当然 Android 提供了一套完整的 API,允许应用程序自由地发送和接收广播。
2、广播机制分类
Android 中的广播主要可以分为两种类型:无序广播 和 有序广播。
无序广播也叫标准广播(Normal broadcasts),它是一种完全异步执行的广播,在广播发出之后,所有的广播接收器几乎都会在同一时刻接收到这条广播消息,因此它们之间没有任何先后顺序可言。这种广播的效率会比较高,但同时也意味着它是无法被截断的。
标准广播的工作流程如下图所示:
有序广播(Ordered broadcasts) 则是一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完毕后,广播才会继续传递。比如有三个广播接收者 A、B、C,优先级是 A > B > C。那这个消息先传给 A,再传给 B,最后传给 C。每个接收者有权终止广播,比如 B 终止广播,C 就无法接收到。所以此时的广播接收器是有先后顺序的,优先级高的广播接收器就可以先收到广播消息,并且前面的广播接收器还可以截断正在传递的广播,这样后面的广播接收器就无法收到广播消息了。
有序广播的工作流程如下图所示:
有序广播和无序广播的共同点:
内部得的实现机制是相同的:通过 Android 系统的 Binder 机制实现通信。
3、关于 BroadCastReceiver 的生命周期
- 相比于我们之前提到的Activity和Fragment的生命周期,广播接收者的生命周期是非常短暂的,在接收到广播的时候创建,onReceive()方法结束之后销毁;
- 广播接收者中不要做一些耗时的工作,否则会弹出 Application No Response 错误对话框;
- 最好也不要在广播接收者中创建子线程做耗时的工作,因为广播接收者被销毁后进程就成为了空进程,很容易被系统杀掉;
- 耗时的较长的工作最好放在服务中完成;
掌握了这些基本概念后,我们就可以来尝试一下广播的用法了。
二、接收系统广播
Android 内置了很多系统级别的广播,我们可以在应用程序中通过监听这些广播来得到各种系统的状态信息。比如手机开机完成后会发出一条广播,电池的电量发生变化会发出一条广播,时间或时区发生改变也会发出一条广播等等。如果想要接收到这些广播,就需要使用广播接收器,下面我们就来看一下它的具体用法。
1、动态注册监听网络变化
广播接收器可以自由地对自己感兴趣的广播进行注册,这样当有相应的广播发出时,广播接收器就能够收到该广播,并在内部处理相应的逻辑。注册广播的方式一般有两种,在代码中注册和在 AndroidManifest.xml 中注册,其中前者也被称为动态注册,后者也被称为静态注册。
那么该如何创建一个广播接收器呢?其实只需要新建一个类,让它继承自 BroadcastReceiver,并重写父类的 onReceive()方法就行了。这样当有广播到来时, onReceive()方法就会得到执行,具体的逻辑就可以在这个方法中处理。
修改 MainActivity 中的代码,如下所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
private void init() {
mIntentFilter = new IntentFilter();
mIntentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
mNetworkChangeReceiver = new NetworkChangeReceiver();
registerReceiver(mNetworkChangeReceiver, mIntentFilter);
}
/**
* 接收系统广播:动态的监听网络变化
*/
private class NetworkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
ConnectivityManager connectionManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectionManager.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isAvailable()) {
Toast.makeText(context, "网络可用!", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "网络不可用!", Toast.LENGTH_SHORT).show();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(mNetworkChangeReceiver);
}
可以看到,我们在 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 的广播,也就实现了监听网络变化的功能。
然后我们通过 getSystemService() 方法得到了 ConnectivityManager 的实例,这是一个系统服务类,专门用于管理网络连接的。然后调用它的 getActiveNetworkInfo() 方法可以得到 NetworkInfo 的实例,接着调用 NetworkInfo 的 isAvailable() 方法,就可以判断出当前是否有网络了,最后我们还是通过 Toast 的方式对用户进行提示。
另外,这里有非常重要的一点需要说明, Android 系统为了保证应用程序的安全性做了规定,如果程序需要访问一些系统的关键性信息,必须在配置文件中声明权限才可以,否则程序将会直接崩溃,比如这里查询系统的网络状态就是需要声明权限的。方法是:打开AndroidManifest.xml 文件,在 最后要记得,动态注册的广播接收器一定都要取消注册才行,这里我们是在 onDestroy() 方法中通过调用 unregisterReceiver()方法来实现的。 动态注册的广播接收器可以自由地控制注册与注销,在灵活性方面有很大的优势,但是它也存在着一个缺点,即必须要在程序启动之后才能接收到广播,因为注册的逻辑是写在 onCreate() 方法中的。那么有没有什么办法可以让程序在未启动的情况下就能接收到广播呢?这就需要使用静态注册的方式了。 这里我们准备让程序接收一条开机广播,当收到这条广播时就可以在 onReceive() 方法里执行相应的逻辑,从而实现开机启动的功能。 可以看到,这里不再使用内部类的方式来定义广播接收器,因为我们需要在 AndroidManifest.xml 中将这个广播接收器的类名注册进去: 终于, 最后别忘了添加相应的权限: 在发送广播之前,我们还是需要先定义一个广播接收器来准备接收此广播才行, 不然发出去也是白发。 注意:因为此处代码我们定义成了 MainActivity 的内部类,所以该广播接收者应该定义成static的,否则的话会报错并引起应用崩溃。 然后在 AndroidManifest.xml 中对这个广播接收器进行静态注册: 可以看到,这里让 MyBroadcastReceiver 接收一条值为 com.willflow.broadcasttest.MY_BROADCAST 的广播,因此我们在发送广播的时候,需要发出这样的一条广播: 可以看到,我们在按钮的点击事件里面加入了发送自定义广播的逻辑。首先构建出了一个 Intent 对象,并把要发送的广播的值传入,然后调用了 Context 的 sendBroadcast()方法将广播发送出去,这样所有监听 com.willflow.broadcasttest.MY_BROADCAST 这条广播的广播接收器就会收到消息,此时发出去的广播就是一条标准广播。 这样我们就成功完成了发送自定义广播的功能。另外,由于广播是使用 Intent 进行传递的,因此我们还可以在 Intent 中携带一些数据传递给广播接收器。 广播是一种可以跨进程的通信方式,这一点从前面接收系统广播的时候就可以看出来了,因此在我们应用程序内发出的广播,其他的应用程序应该也是可以收到的。那么如何如何做到只有特定的应用的广播接受者才能接收到广播呢?这其实就用到了有序广播。 可以看到,发送有序广播只需要改动一行代码,即将 sendBroadcast()方法改成 sendOrderedBroadcast()方法就可以了。 sendOrderedBroadcast()方法接收两个参数,第一个参数仍然是Intent,第二个参数是一个与权限相关的字符串,这里传入 null 就行了。那么该如何设定广播接收器的先后顺序呢?当然是在注册的时候进行设定的了。 可以看到,我们通过 android:priority 属性给广播接收器设置了优先级,优先级比较高的广播接收器就可以先收到广播,这里将 MyBroadcastReceiver 的优先级设成了 100,以保证它一定会在 AnotherBroadcastReceiver 之前收到广播。既然已经获得了接收广播的优先权,那么 MyBroadcastReceiver 就可以选择是否允许广播继续传递了。 我们在 onReceive() 方法中调用了 abortBroadcast() 方法,表示将这条广播截断,后面的广播接收器将无法再接收到这条广播。所以我们可以再注册一个相同的自定义广播接受者,代码如下: 同样需要进行静态注册: 注意,这里我们没有指定优先级别。 当然这个广播其实也是可以在另起一个App进行注册的,不过我们看到的效果将会是同样的,因为前面说过了:广播是一种可以跨进程的通信方式。另外,我们提供一种简单的判断接收到的广播是否为有序广播的方式:在 BroadcastReceiver 类中 onReceive() 方法中调用 boolean b = isOrderedBroadcast() ,这个方法用于告诉我们当前的接收到的广播是否为有序广播。 最后,我们说一下 sendOrderedBroadcast 的另一个重载函数。我们在通过 Context.sendOrderedBroadcast(intent, receiverPermission, resultReceiver, scheduler, initialCode, initialData, initialExtras) 发送广播时,我们可以指定 resultReceiver 广播接收者,这个接收者我们可以认为是最终接收者,通常情况下如果比他优先级更高的接收者如果没有终止广播,那么他的 onReceive 会被执行两次,第一次是正常的按照优先级顺序执行接收,第二次是作为最终接收者接收,如果比它优先级高的接收者终止了广播,那么它依然能接收到广播。 前面我们发送和接收的广播全部都是属于系统全局广播,即发出的广播可以被其他任何的任何应用程序接收到,并且我们也可以接收来自于其他任何应用程序的广播。这样就很容易会引起安全性的问题,比如说我们发送的一些携带关键性数据的广播有可能被其他的应用程序截获,或者其他的程序不停地向我们的广播接收器里发送各种垃圾广播。 为了能够简单地解决广播的安全性问题, Android 引入了一套本地广播机制,使用这个机制发出的广播只能够在应用程序的内部进行传递,并且广播接收器也只能接收来自本应用程序发出的广播,这样所有的安全性问题就都不存在了。本地广播的用法并不复杂,主要就是使用了一个 LocalBroadcastManager 来对广播进行管理,并提供了发送广播和注册广播接收器的方法。 其实这基本上就和我们前面所学的动态注册广播接收器以及发送广播的代码是一样的。只不过现在首先是通过 LocalBroadcastManager 的 getInstance() 方法得到了它的一个实例,然后在注册广播接收器的时候调用的是 LocalBroadcastManager 的 registerReceiver() 方法,在发送广播的时候调用的是 LocalBroadcastManager 的 sendBroadcast() 方法,仅此而已。 另外还有一点需要说明,本地广播是无法通过静态注册的方式来接收的。其实这也完全可以理解,因为静态注册主要就是为了让程序在未启动的情况下也能收到广播,而发送本地广播时,我们的程序肯定是已经启动了,因此也完全不需要使用静态注册的功能。 其实这个问题之前我们已经回答过了,这里我们再提供几种方式。 自己的应用(假设名称为应用 A)在发送广播的时候给自己发送的广播添加自定义权限,假设权限名为: 其他应用(假设名称诶应用 B)如果想接收该广播,那么就必须知道应用 A 广播使用的权限。然后在应用 B 的清单文件中如下配置: 或者在应用 AndroidManifest.xml 中的 上述四类权限级别同样可用于自定义权限中,如果我们需要对自己的应用程序(或部分应用)进行访问控制,则可以通过在 AndroidManifest.xml 中添加 广播的优先级推荐的范围是:[-1000,+1000],而且广播的优先级对无序广播其实也是生效的,有可能你会觉得这很奇怪,但是还有更让人奇怪的,那就是如果设置的优先级值超过这个范围也是可以的。另外关于优先级还有一个值得注意的问题,那就是动态注册的广播优先级谁高呢?答案是:谁先注册谁优先级高。也就是我们所说的先来后到原则。 在Android 4.3以上的版本,允许我们将应用安装在SD上,我们都知道是系统开机间隔一小段时间后,才装载SD卡的,这样我们的应用就可能监听不到这个广播了。所以我们需要既监听开机广播又监听SD卡挂载广播才可以解决这个问题。 另外,有些手机可能并没有SD卡,所以这两个广播监听我们不能写到同一个Intetn-filter里面,而是应该写成两个,配置代码如下: 最后,给大家提供下常用的系统广播,你可以到下篇查看! 点此进入:GitHub开源项目“爱阅”。 感谢优秀的你跋山涉水看到了这里,欢迎关注下让我们永远在一起!编译运行看效果:
2、静态注册实现开机启动
新建一个 BootCompleteReceiver ,代码如下:
/**
* 接收系统广播:静态的监听系统自启动
*/
public class BootCompleteReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "接收到开机自启动广播!", Toast.LENGTH_LONG).show();
}
}
三、发送自定义广播
1、发送标准广播
新建一个 MyBroadcastReceiver,代码如下:
/**
* 接收自定义广播
*/
public static class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "接收到自定义广播!", Toast.LENGTH_SHORT).show();
}
}
private void initView() {
mButton = (Button) findViewById(R.id.buttonPanel);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent("com.willflow.broadcasttest.MY_BROADCAST");
sendBroadcast(intent);
}
});
}
编译运行看效果:
2、发送有序广播
更改 MainActivity 中的发送广播代码如下:
private void initView() {
mButton = (Button) findViewById(R.id.buttonPanel);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent("com.willflow.broadcasttest.MY_BROADCAST");
// 发送有序广播
sendOrderedBroadcast(intent, null);
}
});
}
修改 AndroidManifest.xml 中的代码,如下所示:
修改 MyBroadcastReceiver 中的代码,如下所示:
/**
* 接收自定义广播
*/
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "接收到自定义广播!", Toast.LENGTH_SHORT).show();
// 终止广播的传递
abortBroadcast();
}
}
public class AnotherBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "在另一个广播接受者中接收到自定义广播!", Toast.LENGTH_SHORT).show();
}
}
四、使用本地广播
修改 MainActivity 中的代码,如下所示:
private void initView() {
mButtonLocal = (Button) findViewById(R.id.buttonLocal);
mButtonLocal.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent("com.willflow.broadcasttest.LOCAL_BROADCAST");
// 发送本地广播
mLocalBroadcastManager.sendBroadcast(intent);
}
});
}
private void init() {
mIntentFilterLocal = new IntentFilter();
mIntentFilterLocal.addAction("com.willflow.broadcasttest.LOCAL_BROADCAST");
mLocalReceiver = new LocalReceiver();
mLocalBroadcastManager = LocalBroadcastManager.getInstance(this);
mLocalBroadcastManager.registerReceiver(mLocalReceiver, mIntentFilterLocal);
}
/**
* 接收本地广播
*/
private class LocalReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "接收到本地广播!", Toast.LENGTH_LONG).show();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mLocalBroadcastManager.unregisterReceiver(mLocalReceiver);
}
编译运行看效果:
五、使用广播的几点问题
1、如何让自己的广播只让指定的 App 接收?
(1)添加权限
com.willflow.android.permission
,然后需要在应用 A 的 AndroidManifest.xml 中声明如下权限:
每个权限通过 protectionLevel 来标识保护级别:
2、广播的优先级对无序广播生效吗?
3、Android 4.3 以上版本监听开机启动广播的问题解决