Android开发学习 -- Day11 BroadcastRe
经过之前的练习,我们已经基本将Android UI的相关知识都学习过了。今天将开始介绍Android的下一个非常重要的组件:BroadcastReceiver。
记得我们以前在学校时,每个教室都有一个大喇叭,教导处要发布什么重要信息时,就会在喇叭里播一条广播来告知全校的师生。为了便于进行系统级别的消息通知,Android也引入了一套类似的广播消息机制。
一、广播机制简介
在Android中,广播机制比之前例子中的要更加灵活,因为每个Android程序都可以对自己感兴趣的广播进行注册,这样该程序就只会接收到自己所关心的广播内容,这些广播可能来自系统,也可能来自其他的应用程序。Android提供了一套完整的API,允许应用程序自由地发送和接收广播。发送广播的方法其实之前稍微有接触过一点,就是借助我们学习过的Intent。而接收广播则需要引入一个新的概念 -- BroadcastReceiver。
这里我们先了解一下广播的类型,Android中的广播主要可以分为两种类型:标准广播和有序广播。
□ 标准广播(Normal Broadcast):是一种完全异步执行的广播,在广播发出后,所有的广播接收器几乎都会在同一时刻接收到这条广播消息,因为它们之间没有任何先后顺序。这种广播的效率比较高,但同时也意味着无法被截断。
□ 有序广播(Ordered Broadcast):则是一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完成后,这条广播才会继续传递。所以此时的广播是有先后顺序的,优先级高的广播接收器就可以先收到广播消息,并且前面的广播还能接收器还可以截断正在传递的广播,这样后面的广播接收器就无法收到广播消息了。
借用郭神的图二、接收系统广播
Android 内置了很多系统级别的广播, 我们可以在应用程序中通过监听这些广播来得到各种系统的状态信息。比如手机网络状态发生变化时,会发送广播等等。接下来我们就看一下它的具体用法。
1、动态注册监听网络变化
BroadcastReceiver可以自由的对自己感兴趣的广播进行注册,这样当有相应的广播发出时,广播接收器就能收到该广播,并在内部处理相应的逻辑。注册广的方式一般有两种,在代码中注册和在AndroidManifest中注册,其中前者也被成为动态注册,后者被成为静态注册。
创建一个广播接收器很简单,只需要新建一个类,让它继承自BroadcastReceiver,并重写父类的onReceive()方法就行了。这样当有个别到来时,onReceive()方法就会得到执行,具体的逻辑就可以在这个方法中处理。
我们先通过动态注册的方式编写一个能够监听网络变化的程序,借此学习一下广播接收器的基本用法。
public class NewworkChangeActivity extends BaseActivity {
private IntentFilter intentFilter;
private NetworkChangeReceiver networkChangeReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_newwork_change);
setTitle("networkChangeReceiver");
intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
networkChangeReceiver = new NetworkChangeReceiver();
registerReceiver(networkChangeReceiver, intentFilter);
}
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 Available", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "Network Unavailable", Toast.LENGTH_SHORT).show();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(networkChangeReceiver);
}
}
我们在内部定义了一个NetworkChangeReceiver的内部类,它继承自BroadcastReceiver,并且我们重写了onReceive()方法。这样,每当网络状态发生变化是,onReceive()方法就会得到执行。首先通过getSystemService()方法得到了ConnectivityManager的实例,然后调用了它的getActiveNetworkInfo()方法获取到NetworkInfo的实例,接着通过isAvailable()方法判断当前是否有网络连接,最后通过Toast展示出来。
接下来,我们在onCreate()方法中首先创建了一个intentFilter的实例,并给它添加了一个值为android.net.conn.CONNECTIVITY_CHANGE的action。当系统网络状态发生变化时,系统发出的正式一条值为android.net.conn.CONNECTIVITY_CHANGE的关闭。也就是我们的BroadcastReceiver先搞监听什么广播,就在这里添加响应的action。接下来创建一个NetworkChangeReceiver的实例,然后调用registerReceiver()方法进行注册,将NetworkChangeReceiver实例和IntentFilter的实例传进去,就完成了监听网络变化的功能。
此外,通过动态注册的BroadcastReceiver一定要取消注册才行。这里我们在onDestroy() 方法中是通过调用unregisterReceiver()方法来实现的。
最后重点是,Android系统为了保护用户设备的安全和隐私,做了严格的规定:如果程序需要进行一些对用户来说比较敏感的曹组,就必须在配置文件中声明权限才可以,否则程序会直接崩溃。比如我们这里的访问网络状态,就是需要进行声明的。修改AndroidManifest,
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.johnhao.listviewdemo">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
...
</manifest>
重新启动程序,修改网络的状态:
2、静态注册实现开机自启
动态注册的BroadcastReceiver可以自由的控制注册于注销,在灵活性上有很大优势,但是也存在一个缺点,就是必须得等程序启动后才能接收广播,因为注册逻辑是写在onCreate()方法中的。而通过静态注册的方法,我们就可以让程序在未启动的情况下就能接收到广播。
通过Android Studio提供的快捷方式来创建一个BroadcastReceiver,New -> Other -> BroadcastReceiver
因为是开机自启的,我们将其命名为BootCompleteReceiver,Exported属性表示是否允许这个BroadcastReceiver接收本程序以外的广播,Enable属性表示是否启动这个BroadcastReceiver。点击Finish,完成创建:
public class BootCompleteReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// TODO: This method is called when the BroadcastReceiver is receiving
// an Intent broadcast.
Toast.makeText(context, "Boot Complete", Toast.LENGTH_LONG).show();
throw new UnsupportedOperationException("Not yet implemented");
}
}
代码很简单,我们在onReceive()方法中加一个Toast提示。另外,静态注册的BroadcastReceiver一定要在AndroidManifest中注册才可以使用。不过因为我们用Android Studio的快捷方式创建的,因此它已经自动帮我们注册好了,打开AndroidManifest查看:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.johnhao.listviewdemo">
...
<application
...
<receiver
android:name=".Receiver.BootCompleteReceiver"
android:enabled="true"
android:exported="true"></receiver>
</application>
</manifest>
可以看到,<application>标签内出现了一个新的标签 <receiver>,所有的静态BroadcastReceiver都是在这里进行注册的。不过目前还是不能接收到开机广播的,还需要进一步修改:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.johnhao.listviewdemo">
...
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<application
...
<receiver
android:name=".Receiver.BootCompleteReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
</application>
</manifest>
由于Android系统启动完毕之后,会发出一条值为android.intent.action.BOOT_COMPLETED的广播,因为我们在<intent-filter>标签里添加了相应的<action>。另外监听系统的开机广播也需要声明权限,在uses-permission中添加权限android.permission.RECEIVE_BOOT_COMPLETED。重新运行程序后,我们的应用就可以接收开机广播了。
需要特别注意的是,不要在广播接收器的onReceive()方法中添加过多复杂的逻辑或者耗时的操作。因为广播是不允许开线程的,当onReceive()方法执行了较长时间,而没有结束,程序就会出现异常。
三、发送自定义广播
1、发送标准广播
在发送广播前,我们先需要定义一个广播接收器, 要不广播也是白发。新建MyBroadcastReceiver类,让他继承自BroadcastReceiver:
public class MyBroadcastReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received in MyBroadcastReceiver", Toast.LENGTH_SHORT).show();
}
}
然后修改AndroidManifest文件,
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.johnhao.listviewdemo">
<application
...
<receiver
android:name=".Receiver.MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter android:priority="100">
<action android:name="com.johnhao.listviewdemo.MY_BROADCAST" />
</intent-filter>
</receiver>
</application>
</manifest>
让MyBroadcastReceiver接收一条com.johnhao.listviewdemo.MY_BROADCAST的广播。
public class MyBroadcastReceiverActivity extends BaseActivity {
private Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_broadcast_receiver);
setTitle("MyBroadcast Receiver");
btn = findViewById(R.id.btn_send_mybroadcast);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.johnhao.listviewdemo.MY_BROADCAST");
sendBroadcast(intent);
}
});
}
}
给按钮注册一个监听器,然后在点击事件里实现发送自定义广播。首先创建一个Intent对象,并发要发送的广播传进去,然后调用Context的sendBroadcast()方法将广播发出。这样所有监听com.johnhao.listviewdemo.MY_BROADCAST的广播接收器就会收到消息。
重新运行一下程序,点击按钮发送广播:
2、发送有序广播
稍微修改一下代码,
public class MyBroadcastReceiverActivity extends BaseActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
btn_ordered.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent ordered_intent = new Intent("com.johnhao.listviewdemo.MY_BROADCAST");
sendOrderedBroadcast(ordered_intent, null);
}
});
}
}
新定义一个Button,并设置点击事件。同样是构建Intent对象,并传入值,然后不同的地方在于我们调用了sendOrderedBroadcast()方法。该方法接收两个参数,第一个参数是Intent,第二个参数是一个与权限相关的字符串,这里传入null就行了。接下来新建个广播接收器:
public class AnotherBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received in AnotherBroadcastReceiver", Toast.LENGTH_SHORT).show();
// 有序广播 截断广播
abortBroadcast();
}
}
同样的只弹一个Toast,然后在调用onReceive()方法中调用abortBroadcast()方法来中断广播。别忘了修改AndroidManifest文件:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.johnhao.listviewdemo">
<application
<receiver
android:name=".receiver.AnotherBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter android:priority="100">
<action android:name="com.johnhao.listviewdemo.MY_BROADCAST" />
</intent-filter>
</receiver>
</application>
</manifest>
先通过android:prioritys属性给广播接收器设置了优先级,用来保证AnotherBroadcastReceiver先于MyBroadcastReceiver接收到广播。这样,在发送有序广播时,AnotherBroadcastReceiver会先收到广播,并终止广播的传递,MyBroadcastReceiver就接收不到消息了。
重新运行下程序:
可以看到, 在发送标准广播的时候,因为AnotherBroadcastReceiver设置了优先级,所以先接收到消息,之后MyBroadcastReceiver也收到消息,所以连续弹了两个Toast;当发送有序的广播的时候,在AnotherBroadcastReceiver里我们执行了abortBroadcast()操作,所以MyBroadcastReceiver收不到消息,最后只有AnotherBroadcastReceiver一个Toast提示。
四、使用本地广播
前面我们发送和接收的广播全属于系统全局广播,即发出的广播可以被其他任何应用程序接收到。但这样容易引起安全问题,如果我们发送了一些带有关键数据的广播,很可能就被其他应用截获,或者被其他恶意应用灌入大量垃圾广播。
还好,Android系统引入了一套本地广播机制,使用这个机制发出的广播只能够在本应用内部进行传递,并且广播接收器也只能接受自本应用程序发出的广播。
下面来一看一下本地广播的使用方法:
public class LocalBroadcastActivity extends AppCompatActivity {
private IntentFilter intentFilter;
private LocalReceiver localReceiver;
private LocalBroadcastManager localBroadcastManager;
private Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_local_broadcast);
setTitle("内部广播");
btn = findViewById(R.id.btn_send_internal_broadcast);
localBroadcastManager = LocalBroadcastManager.getInstance(this);
intentFilter = new IntentFilter();
intentFilter.addAction("com.johnhao.listviewdemo.LOCAL_BROADCAST");
localReceiver = new LocalReceiver();
localBroadcastManager.registerReceiver(localReceiver, intentFilter);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.johnhao.listviewdemo.LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(intent);
}
});
}
class LocalReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Received in LocalReceiver", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
localBroadcastManager.unregisterReceiver(localReceiver);
}
}
和动态注册广播接收器很类似,却别是先通过LocalBroadcastManager的getInstance()方法获取到它的实例,然后在注册广播接收器的时候调用的是LocalBroadcastManager的registerReceiver()方法,发送的时候调用LocalBroadcastManager的sendBroadcast()方法,注销时调用LocalBroadcastManager的unregisterReceiver()方法。我们发送一条com.johnhao.listviewdemo.LOCAL_BROADCAST广播,然后在LocalReceiver去接收这条广播。
重新运行程序:
我们可以新写个应用程序,去验证是否能在新app中收到这条广播。
最后总结下本地广播的几个优点:
□ 广播在应用程序内部,不用担心泄密等安全问题
□ 其他应用的广播无法发送到我们的应用内部
□ 发送本地广播比全局广播更高效