第五章 详解广播机制
广播机制简介
Android提供了一套完整的API,允许应用程序自由地发送和接收广播。发送广播的方法可以借助之前学到的Intent。而接收广播的方法则需要引入一个新的概念——广播接收器(Broadcast Receiver)。
广播的类型
-
标准广播:是一种完全异步执行的广播,在广播发出之后,所有的广播接收器几乎都会在同一时刻接收到这条消息。因此它们之间没有任何先后顺序。这种在广播i的效率回避囧高,但同时也意味着他是无法被截断的。
-
有序广播:则是一种同步执行的广播,在广播发出之后,同一时刻只有一个广播接收器能够收到这条广播消息,当广播接收器中的逻辑执行完之后,广播才会继续传递。广播接收器有优先级和接收的先后顺序。可以中间截断。
接收系统广播
动态注册监听网络变化
注册广播的方式一般有两种:在代码中注册和在AndroidManifest.xml中注册,其中前者也被称为动态注册,后者被称为静态注册。
1.动态接收。5步。
public class MainActivity extends AppCompatActivity {
//1.创建一个类继承自BroadcastReceiver,即创建广播接收器,
// 当有广播到来时,onReceive()方法就会得到执行,在这个方法中处理收到广播后的逻辑。
class NetworkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "network changes", Toast.LENGTH_SHORT).show();
}
}
private IntentFilter intentFilter;
private NetworkChangeReceiver networkChangeReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//2.获取IntentFilter的实例,添加相应的action
intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
//3.创建BroadcastReceiver类的子类对象
networkChangeReceiver = new NetworkChangeReceiver();
//4.注册
registerReceiver(networkChangeReceiver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
//5.取消注册
unregisterReceiver(networkChangeReceiver);
}
}
-
在MainActivity中定义一个内部类NetworkChangeReceiver,这个类是继承于BroadcastReceiver的,并重写了父类的onReceiver()方法。接收到广播之后就会执行这个方法中的逻辑。
-
在onCreate()方法中,我们先创建了一个IntentFilter的实例,并调用方法addAction()给他添加了一个值android.net.conn.CONNECTIVITY_CHANGE的action。这个action表示当网络状态发生变化时,系统所发出的广播。我们想要广播接收器监听什么广播,就在这里添加相应的action。
-
接下来创建一个NetworkChangeReceiver的实例。
-
然后调用registerReceiver()方法进行注册,将两个实例都传递进去。
-
在重写的onDestroy()方法中调用unregisterReceiver()方法,传递NetworkChangeReceiver的实例作为参数。当程序关闭或者活动销毁的时候就会取消广播接收器的注册。
我们可以改写onReceive()中的代码,让弹窗提醒更人性化。
@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();
}
}
在onReceive()中
-
通过getSystemService()方法得到了ConnectivityManager的实例,这是一个系统服务类,专门用于管理网络连接的。
-
然后调用它的getActiveNetWorkInfo()方法可以得到NetworkInfo的实例;
-
接着调用NetworkInfo的isAvailable()方法。可以判断出当前是否有网络连接。
-
最后通过Toast向用户提示。
另外说明一点。Android系统为了保护用户设备的安全和隐私,做了严格的规定:如果程序需要进行一些对用户来说比较敏感的操作,就必须在配置文件中生命权限才可以,否则程序将会直接崩溃。例如这里的访问系统网络状态就是需要声明权限的。需要在AndroidManifest.xml文件中加入权限。
如果没有这一句在代码中也会报错。
静态注册实现开机启动
动态注册的广播接收器可以自由的控制注册于注销,在灵活性方面有很大优势,但是他也存在这一个缺点,即必须要在程序启动之后才能接收广播。因为它的注册和销毁是由活动的onCreate()方法和onDestroy()方法控制的。若要在程序未启动的时候接收广播,就要用到静态注册的方法了。
静态注册的用法。
在Java的项目包下New/Other/BroadcastReceiver。可以自动帮我们创建一个继承于BroascastReceiver的类。我们只需要在这个类的onReceive()中编写逻辑即可。
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Boot Complete", Toast.LENGTH_SHORT).show();
}
我们在这里简单的打印以下就可以了。
之后要在AndroidManifest.xml文件中修改代码。
这个receiver标签就是Android studio为我们添加的静态注册。
我们在这个标签中添加代码。
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
在receiver标签中添加一个intent-filter标签,嵌套一个action标签。在action标签内添加属性 android:name=""。引号中就放入想要接收的广播消息。这里的android.intent.action.BOOT_COMPLETED是Android系统在启动完成后发出的一条广播。
属性android:exported表示是否允许这个广播接收器接受本程序以外的广播;
属性android:enabled表示是否允许启用这个广播接收器。
另外,监听系统开机广播也是需要声明权限的。我们在文件中加入这段代码。
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
注意:在onReceive()方法中不要添加过多的逻辑或者进行任何耗时的操作。因为在广播接收器中是不允许开启线程的,当onReceive()方法运行了较长时间而没有结束之,程序就会报错。
发送自定义广播
发送标准广播
在发送广播之前,先定义一个接收器用来准备接受此广播。
用刚才静态注册的方法新建一个广播接收器MyBroadcastReceiver。
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "收到了", Toast.LENGTH_SHORT).show();
}
}
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.example.broadcasttest.My_BROADCAST"/>
</intent-filter>
</receiver>
然后在AndroidManifest.xml文件中找到receiver标签,添加intent-filter,再添加action。这里就设定一个特定的信息用来接收广播。用intent发送广播的时候要保证这两条信息一致。
<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:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/button"
android:layout_gravity="center"
android:text="Send Broadcast"/>
</LinearLayout>
我们重新写一下布局。设置一个按钮,点击按钮发送广播。
在MainActivity中实现发送广播的逻辑。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
@SuppressLint({"MissingInflatedId", "LocalSuppress"})
Button b = (Button) findViewById(R.id.button);
b.setOnClickListener(view -> {
Intent intent = new Intent("com.example.broadcasttest.My_BROADCAST");
intent.setPackage(getPackageName());
sendBroadcast(intent);
});
}
注意:这里intent传入的字符串要求与在AndroidManifest文件中的标签的一样。
这里程序运行,点击按钮没有反应。加上intent.setPackage(getPackageName());
【Broadcast】Android8.0 静态receiver接收不到隐式广播
这样程序就能正常运行了。
发送有序广播
广播是一种可以跨进程的通信方式。因此我们在应用程序内发出的广播,其他的应用程序应该也是可以收到的。
写法一样。在其他项目中建一个静态的广播接收器。然后传入相同的intent-filter的内容。
注:由于Android新版本的特性,发送广播需要指定包,我们在发送广播的时候给intent指定了本应用程序的包,所以广播在别的程序中接收不到,在测试时可换成同一程序测试接收顺序,或者参考下面的博客。
《第一行代码》第5.3.1发送标准广播无法接收到及发送广播无法在另一个应用程序中接收到,解决方案
这个解决办法相当于发送了两条广播;
这个解决方法要改成动态接收器。
有序广播:
Intent intent = new Intent("com.example.broadcasttest.My_BROADCAST");
intent.setPackage(getPackageName());
sendOrderedBroadcast(intent, null);
发送有序广播时把sendBroadcast(intent);换成sendOrderedBroadcast(intent, null);
第一个参数仍然是Intent,蝶儿个参数是一个与权限相关的字符串。我们自定义的广播这里不需要权限,所以直接传null就可以。
在注册时设定广播接收器的优先顺序:
修改AndroidManifest.xml文件中的代码。
在自定义的静态接收器的< receiver>标签的< intent-filter>标签的内添加属性:android:priority=""。优先级较高的广播就可以先收到广播。
选择是否阻断广播:
既然获得了接收广播的优先权,MyBroadcastReceiver就可以选择是否继续传递广播了。修改MyBroadcastReceiver中的代码。
调用abortBroadcast()方法表示这条广播被截断。
注:Android8.0以上更改了关于广播的特性。
发送自定义广播时,静态注册广播接收器要接收到广播必须在发出端设置intent的setPackage()或者setComponent()指定发送的包名。
setPackage()的参数只有一个,指定发送到的包名。
setComponent()的参数有两个,前一个参数是应用程序的包名,后一个是这个应用程序的主活动类名。
因为一个intent只能使用setPackage()指定一个包名。所以当同一条自定义广播发给一个静态注册的广播接收器之后就没法发给其他接收器了。要么使用intent多次发送广播,要么将所有接收器都改为动态注册的。
使用本地广播
前面我们发送和接收的广播全部属于系统全局广播,即发出的广播可以被其他任何应用程序接收到,并且我们也可以接收来自其他应用程序的广播。这样就很容易引起安全性的问题,比如说我们发送的一些携带关键性数据的广播可能被其他应用程序截获,或者其他应用程序不停地向我们的广播接收器里发送各种没用的广播。
为了简单的解决广播的安全性问题,Android引入了一套本地的广播机制,使用这个机制发出的广播只能够在应用程序内部进行传递,并且广播接收器也只能接收来自本应用程序发出的广播。
使用本地广播:
先将依赖包导入build.gradle文件中。'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' (不知道是不是必须导)
其他步骤在MainActivity中实现代码。基本思路和动态注册广播接收器一样。使用LocalBroadcastManager的实例来管理发送、注册的动作。
public class MainActivity extends AppCompatActivity {
private IntentFilter intentFilter;
class LocalReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "接收到了本地广播", Toast.LENGTH_SHORT).show();
}
}
private LocalReceiver localReceiver; //本地接收器的全局变量
private LocalBroadcastManager localBroadcastManager;
//使用前要导 'com.android.support:support-v4:28.0.0'包
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
@SuppressLint({"MissingInflatedId", "LocalSuppress"})
Button button = findViewById(R.id.button);
button.setOnClickListener(view -> {
Intent intent = new Intent("LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(intent);
});//发送广播
//第一步:LocalBroadcastManager的实例
localBroadcastManager = LocalBroadcastManager.getInstance(this); //获取实例
//第二步:获取IntentFilter的实例,并添加相应的action
intentFilter = new IntentFilter("LOCAL_BROADCAST");
//第三步:创建广播接收器的实例
localReceiver = new LocalReceiver();
//第四步:注册
localBroadcastManager.registerReceiver(localReceiver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
//第五步:取消注册
localBroadcastManager.unregisterReceiver(localReceiver);
}
}
LocalBroadcastManager的实例通过LocalBroadcastManager本类的静态方法getInstance()方法获取。参数传递context,上下文。
总之,广播的发送和接收(注册)都有LocalBroadcastManager来管理。其他步骤与动态注册一样
总结本地广播的几点优势:
-
发送的广播不会离开程序,不用担心数据泄露安全;
-
其他的程序无法发送广播进来;
-
发送本地广播比发送系统全局广播更加有效。
广播的最佳实践
实现强制下线功能
实现强制下线功能需要先关闭掉所有的活动,然后返回到登陆页面。这里可以使用我们在第2张活动的最佳实践中的创建管理活动的集合ActivityCollector来帮助实现。
-
我们先来完成一个管理活动的集合类。新建Javaclass ActivityCollector。
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 activity: activityList) { activity.finish(); removeActivity(activity); } } }
添加一个静态的集合字段,用来模拟返回栈,存放活动。
再添加一个添加活动的方法,一个移除活动的方法,这两个方法是针对活动集合的管理。
再添加一个清空活动的方法,这个方法会将所有开启的活动全部停止、销毁。
-
然后创建一个BaseActivity类作为所有活动的父类。
public class BaseActivity extends AppCompatActivity { //java中不能多继承,所以自定义BaseActivity作为所有Activity的父类。在这里面写重写逻辑 @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityCollector.addActivity(this); }//重写onCreate()方法 当活动创建时添加到活动集合中 @Override protected void onDestroy() { super.onDestroy(); ActivityCollector.removeActivity(this); }//重写onDestroy()方法 当活动销毁时从活动集合中移除 }
BaseActivity类继承于AppCompatActivity类。重写它的onCreate()方法和onDestroy()方法。在方法里面分别添加ActivityCollector.addActivity(this)方法和ActivityCollector.removeActivity(this)方法。这样可以自动实现活动进出活动集合。
-
接下来我们需要制作一个简陋的登陆界面。新建空活动LoginActivity。
修改activity_login.xml中的代码。
<LinearLayout 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:layout_width="match_parent" android:layout_height="match_parent" tools:context=".LoginActivity" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="10dp" android:text="Account: " android:layout_gravity="center_vertical" android:textSize="20sp" /> <EditText android:id="@+id/et_account" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:hint="Type your account." android:layout_gravity="center_vertical" tools:ignore="DuplicateIds" /> <View android:layout_width="60dp" android:layout_height="40dp" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="10dp" android:text="Password: " android:layout_gravity="center_vertical" android:textSize="20sp" /> <EditText android:id="@+id/et_password" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:hint="Type your password." android:layout_gravity="center_vertical"/> <View android:layout_width="60dp" android:layout_height="40dp" /> </LinearLayout> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Login" android:id="@+id/button_login"/> </LinearLayout>
再修改LoginActivity Java类中的代码。
public class LoginActivity extends BaseActivity { private EditText accEt; private EditText pasEt; private Button login; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); accEt = (EditText) findViewById(R.id.et_account); pasEt = (EditText) findViewById(R.id.et_password); login = (Button) findViewById(R.id.button_login); login.setOnClickListener(view -> { String account = accEt.getText().toString();//已知账号admin String password = pasEt.getText().toString();//已知密码123456 if(account.equals("admin") && password.equals("123456")){ Toast.makeText(this, "登陆成功", Toast.LENGTH_SHORT).show(); Intent intent = new Intent(this, MainActivity.class); startActivity(intent); finish(); }else { AlertDialog.Builder ab = new AlertDialog.Builder(this); ab.setTitle("登陆失败!").setMessage("账号或密码错误!").setPositiveButton("重试", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { } }).setNegativeButton("忘记密码", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { AlertDialog.Builder ab2 = new AlertDialog.Builder(LoginActivity.this); ab2.setMessage("账号:admin\n密码:123456").setPositiveButton("OK", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { } }).show(); } }).show(); } }); } }
我们设置默认账号密码为:admin和123456。实现账号密码的检验和提示。
-
登陆后的第一个页面就是MainActivity。我们只需要完成强制下线功能,这里就不模拟其他花里胡哨的页面了。
修改activity_main.xml中的代码。
<LinearLayout 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:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/button_force_offline" android:text="Send force offline broadcast."/> </LinearLayout>
只需要设置一个按钮,让这个按钮实现点击强制下线的功能。
修改MainActivity中的代码。
public class MainActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); @SuppressLint({"MissingInflatedId", "LocalSuppress"}) Button btn = (Button) findViewById(R.id.button_force_offline); btn.setOnClickListener(view -> { Intent intent = new Intent("FORCE_OFFLINE"); sendBroadcast(intent); }); } }
点击按钮发送强制下线的广播。
我们并不把强制下线的逻辑写在MainActivity中,而是卸载接收这条广播的广播接收器里面。这样强制下线功能就不会依附于任何界面,不管在程序的任何地方,只需要发出这样一条广播,就可以强制下线。
-
由于我们想要在程序的任何地方都能够强制下线。因为程序离不开活动,所以我们直接将广播接收器写在这些活动的父类BaseActivity里面。
public class BaseActivity extends AppCompatActivity { //java中不能多继承,所以自定义BaseActivity作为所有Activity的父类。在这里面写重写逻辑 //广播接收器也在这里写,这样就不需要在每个页面写一遍弹窗 class ForceoOfflineReceiver extends BroadcastReceiver{ @Override public void onReceive(Context context, Intent intent) { AlertDialog.Builder ab = new AlertDialog.Builder(context); ab.setTitle("Warning").setMessage("You are forced to be offline.\nPlease try to login again.") .setCancelable(false) .setPositiveButton("OK", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { ActivityCollector.finishAll();//关闭所有活动 Intent intent1 = new Intent(context, LoginActivity.class); context.startActivity(intent1);//重启登陆界面 } }).show(); } } private ForceoOfflineReceiver forceoOfflineReceiver; private IntentFilter intentFilter; @Override protected void onResume() { super.onResume(); intentFilter = new IntentFilter("FORCE_OFFLINE"); forceoOfflineReceiver = new ForceoOfflineReceiver(); registerReceiver(forceoOfflineReceiver, intentFilter); } @Override protected void onPause() { super.onPause(); if(forceoOfflineReceiver != null){ unregisterReceiver(forceoOfflineReceiver); forceoOfflineReceiver = null; } } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityCollector.addActivity(this); }//重写onCreate()方法 当活动创建时添加到活动集合中 @Override protected void onDestroy() { super.onDestroy(); ActivityCollector.removeActivity(this); }//重写onDestroy()方法 当活动销毁时从活动集合中移除 }
在BaseActivity中编写一个动态注册的广播接收器。将注册的过程写在重写的onResume()方法中而不是onCreate()方法中,将取消注册的过程写在重写的onPause()方法中而不是onDestroy()方法中。因为我们始终需要保证只有处于栈顶的活动才能接收这条强制下线的广播,非栈顶活动没有必要去接受这条广播。
-
最后一步。我们进入程序先进到的是登陆界面,所以在AndroidManifest.xml中将LoginActivity改为主活动。
<activity android:name=".LoginActivity" android:exported="true" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".MainActivity" android:exported="false"> </activity>
代码完成。运行正常。