参考书籍:Android第一行代码(第二版).郭霖著
网络通信中,在一个IP网络范围内,最大的IP地址是被保留作为广播地址来使用的。如某网络IP范围是192.168.0.XXX,子网掩码是255.255.255.0,那这个网络的广播地址就是192.168.0.255.广播数据包会被发送到同一网络的所有端口,该网络中的每台主机都会收到这条广播。
Android也引入一套类似的广播消息机制,更加灵活。
1、广播机制
Android中每个应用程序都可对自己感兴趣的广播进行注册,这样只会接收关心的广播内容(来自与系统或其他应用程序)。Android提供了一套完整API让应用程序自由发送(Intent)和接受广播(广播接收器Broadcast Receiver)。
Android中广播主要分为标准广播(完全异步执行,所有广播接收器几乎同时接收,无先后顺序,效率高无法被截断)和有序广播(同步执行,发出后同时有一个广播接收器接收,其逻辑执行完后广播才继续传播,有先后顺序即优先级,可截断,前面的接收器截断,后面就无法接收)。
2、接收系统广播
Android中内置很多系统级别广播,可用来在应用程序中监听得到系统状态信息,如开机、电池电量变化、时间时区改变等都会发出一条广播。要接收就需使用广播接收器。
广播注册方式一般有两种:动态注册(在代码中注册)和静态注册(在AndroidManifest.xml中注册)。
(1)动态注册
创建一个广播接收器:新建一个类继承自BroadcastReceiver,并重写父类的onReceive()方法(当有广播到来时被执行,具体逻可在此处理)即可。
例:动态注册监听网络变化。新建一个BroadcastTest项目,修改主程序:
public class MainActivity extends AppCompatActivity {
private IntentFilter intentFilter;
private NetworkChangReceiver networkChangReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
//网络状态发生变化时,系统发出一条android.net.conn.CONNECTIVITY_CHANGE广播
networkChangReceiver = new NetworkChangReceiver();
registerReceiver(networkChangReceiver,intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(networkChangReceiver);//动态注册的广播接收器一定要取消注册
}
class NetworkChangReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {//每当网络状态发生变化时被执行
Toast.makeText(context,"network changes", Toast.LENGTH_SHORT).show();
}
}
}
运行程序,按下Home键挂起,在Setting—>Data usage中打开/关闭Celluar data来启动/禁止网络,会看到Toast信息。
要能准确告诉用户有/无网络,需进一步优化:
class NetworkChangReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {//每当网络状态发生变化时被执行
ConnectivityManager connectionManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
//ConnectivityManager为系统服务类,专门用于管理网络连接
NetworkInfo networkInfo = connectionManager.getActiveNetworkInfo();//得到NetworkInfo实例
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();
}
}
}
Android系统为保护用户设备安全和隐私,做了严格规定:如果程序需要进行对用户来说比较敏感的操作,必须在配置文件中声明权限,否则程序将直接崩溃。访问系统网络状态需要声明权限,在AndroidManifest.xml中加入如下权限:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.jojo.broadcasttest">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
。。。
manifest>
重新运行程序测试。
(2)静态注册
动态注册灵活性大,但必须在程序启动后才能接收到广播(注册逻辑在onCreate()中)。要让程序在未启动状态下接收广播,需要静态注册。
例:接收开机广播。可使用Android Studio提供的快捷方式创建广播接收器,包->New->Other->Broadcast Receiver,命名即可(Exported表示是否允许接受本程序以外的广播,Enabled表示是否启用这个广播接收器)。修改其中代码:
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_SHORT).show();
}
}
静态广播接收器一定要在AndroidManifest.xml中注册,不过AS快捷方式创建的会自动完成注册。打开此文件可看到:
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">receiver>
标签为,与很相似。
目前还不能接收到开机广播,还需做如下修改:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
。。。
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">
**<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
intent-filter>**
receiver>
。。。
监听系统开机广播需要权限。运行程序,将模拟器关闭重启后会收到开机广播。
注意:不要在onReceive()中添加过多逻辑/进行耗时操作,广播接收器中不允许开启线程,onReceive()运行较长时间没有结束就会报错。所以广播接收器一般用来打开程序其他组件(如创建状态栏通知、启动服务等)。
3、发送自定义广播
(1)发送标准广播
先定义广播接收器用于接收,新建MyBroadcastReceiver:
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context,"received in MyBroadcastReceiver", Toast.LENGTH_SHORT).show();
}
}
在AndroidManifest.xml中对它进行修改:
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
**<intent-filter>
<action android:name="com.example.jojo.broadcasttest.MY_BROADCAST"/>
intent-filter>**
receiver>
修改activity_main.xml中代码,添加一个用于发送广播的按钮。再修改MainActivity中代码,给按钮绑定点击事件:
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 v) {
Intent intent = new Intent("com.example.jojo.broadcasttest.MY_BROADCAST");
sendBroadcast(intent);//发送标准广播
}
});**
。。。
(2)发送有序广播
广播时一种跨进程的通信方式,所以应用程序内发出的光波,其他应用程序也可以收到。
新建一个BroadcastTest2项目。在此项目下定义一个广播接收器,用于接收第一个项目中的自定义广播,新建AnotherBroadcastReceiver:
public class AnotherBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context,"received in AnotherBroadcastReceiver", Toast.LENGTH_SHORT).show();
}
}
再注册信息中进行修改:
<receiver
android:name=".AnotherBroadcastReceiver"
android:enabled="true"
android:exported="true">
**<intent-filter>
<action android:name="com.example.jojo.broadcasttest.MY_BROADCAST"/>
intent-filter>**
receiver>
运行程序,然后重新回到BroadcastTest项目主界面,点击按钮,会分别弹出两次提示信息。
由此可见,应用程序发出的广播可以被其他应用程序接收到。
发送有序广播:
修改MainActivity:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.jojo.broadcasttest.MY_BROADCAST");
**sendOrderedBroadcast(intent,null);//发送有序广播,第二个参数是与权限相关的字符串**
}
});
重新运行程序,会发现跟之前效果一样。
但是这个时候广播接收器有先后顺序,前面的接收器可将广播截断阻止其继续传播。广播接收器的先后顺序在注册时进行设定:
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter **android:priority="100"**>
<action android:name="com.example.jojo.broadcasttest.MY_BROADCAST"/>
intent-filter>
receiver>
使用android:priority设置优先级,这里设置为100,保证它一定会在AnotherBroadcastReceiver之前收到广播。活得优先权的接收器可以选择是否允许广播继续传递。修改MyBroadcastReceiver:
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context,"received in MyBroadcastReceiver", Toast.LENGTH_SHORT).show();
**abortBroadcast();//截断广播**
}
}
重新运行程序发现,第二个广播接收器确实没收到广播。
4、本地广播
前面的广播全部属于系统广播(可被任意程序接收,可接收来自任意程序的广播),这容易引起安全问题。
为解决广播安全问题,Android引入一套本地广播机制(本地广播只在应用程序内部传递)。
本地广播主要使用一个LocalBroadcastManager进行管理(提供了发送广播和注册广播接收器的方法)。例,修改MainActivity:
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 v) {
Intent intent = new Intent("com.example.jojo.broadcasttest.LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(intent);//发送本地广播
}
});
intentFilter = new IntentFilter();
intentFilter.addAction("com.example.jojo.broadcasttest.LOCAL_BROADCAST");
localReceiver = new LocalReceiver();
localBroadcastManager.registerReceiver(localReceiver,intentFilter);//注册本地广播监听器
}
class LocalReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context,"received local broadcast", Toast.LENGTH_SHORT).show();
}
}
跟之前的动态注册广播接收器及发送广播的方法类似,只不过用的是LocalBroadcastManager中的方法。
运行程序,点击按钮,会弹出Toast消息:
这时如果想在BroadcastTest2中接收这条广播是不行的。
本地广播无法通过静态注册方式接收,也不需要(发送本地广播时,程序肯定已经启动了)。
优点:不必担心泄露机密数据;其他程序无法将广播发送到我们程序内部,不需担心安全漏洞隐患;发送本地广播比系统全局广播更高效。
5、广播的最佳实践
实现强制下线功能:在界面上弹出一个对话框,让用户无法进行其他操作,必须点击确定按钮回到登录界面即可。
新建BroadcastBestPractice项目,强制下线功能需要先关闭所有的活动,然后回到登录界面。先创建一个ActivityCollector类管理所有活动:
public class ActivityCollector {
public static List activities = new ArrayList<>();
public static void addActivity(Activity activity){
activities.add(activity);
}
public static void removeActivity(Activity activity){
activities.remove(activity);
}
public static void finishAll(){
for (Activity activity : activities){
if (!activity.isFinishing()){
activity.finish();
}
}
}
}
然后创建BaseActivity类作为所有活动的父类:
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("BaseActivity",getClass().getSimpleName());//获取当前实例的类名并打印出来
ActivityCollector.addActivity(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
ActivityCollector.removeActivity(this);
}
}
创建一个登录界面活动LoginActivity,编辑相应布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Account:"/>
<EditText
android:id="@+id/account"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"/>
LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Password:"/>
<EditText
android:id="@+id/password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:inputType="textPassword"/>
LinearLayout>
<Button
android:id="@+id/login"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="Login"/>
LinearLayout>
包含账号信息输入、密码信息输入和登录按钮。
修改LoginActivity:
public class LoginActivity extends BaseActivity {
private EditText accountEdit;
private EditText passwordEdit;
private Button login;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
accountEdit = (EditText)findViewById(R.id.account);
passwordEdit = (EditText)findViewById(R.id.password);
login = (Button)findViewById(R.id.login);
login.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String account = accountEdit.getText().toString();
String password = passwordEdit.getText().toString();
//如果账号是admin,密码是123456,则登录成功
if (account.equals("admin") && password.equals("123456")){
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
startActivity(intent);
finish();
}else {
Toast.makeText(LoginActivity.this, "account or password is invalid", Toast.LENGTH_SHORT).show();
}
}
});
}
}
MainActivity为登录成功后进入的主界面,加入强制下线功能即可,修改activity_main.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/force_offline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send force offline broadcast"
android:textAllCaps="false"/>
LinearLayout>
加了一个用于出发强制下线功能的按钮。修改MainActivity:
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button)findViewById(R.id.force_offline);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.jojo.broadcastbestpractice.FORCE_OFFLINE");//用于通知程序用户下线的广播
sendBroadcast(intent);
}
});
}
}
这里按钮点击事件里发送了一条广播,用于通知程序强制用户下线的,而强制下线的逻辑应该卸载广播接收器里(不依附于任何界面)。
广播接收器需要弹出一个对话框阻塞用户正常操作,但静态注册的广播接收器没有办法在OnReceive()方法里弹出对话框UI控件,也不可能在每个活动里注册动态广播接收器。只需在BaseActivity中动态注册广播接收器就可以了,修改BaseActivity:
public class BaseActivity extends AppCompatActivity {
private ForceOfflineReceiver receiver;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("BaseActivity",getClass().getSimpleName());//获取当前实例的类名并打印出来
ActivityCollector.addActivity(this);
}
@Override
protected void onResume() {
super.onResume();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("com.example.jojo.broadcastbestpractice.FORCE_OFFLINE");
receiver = new ForceOfflineReceiver();
registerReceiver(receiver,intentFilter);
}
@Override
protected void onPause() {
super.onPause();
if (receiver != null){
unregisterReceiver(receiver);
receiver = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
ActivityCollector.removeActivity(this);
}
class ForceOfflineReceiver extends BroadcastReceiver{
@Override
public void onReceive(final Context context, Intent intent) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("Warning");
builder.setMessage("You are forced to be offline.Please try to login again.");
builder.setCancelable(false);//对话框不可取消
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCollector.finishAll();//销毁所有活动
Intent intent = new Intent(context, LoginActivity.class);
context.startActivity(intent);//重新启动登录活动
}
});
builder.show();
}
}
}
这里是在onResume()和onPause()这两个方法中注册和取消注册广播接收器的,因为要始终保证只有处于栈顶的活动才能接收到这条广播,当活动失去栈顶位置时就会自动取消广播接收器的注册。
还需对AndroidManifest.xml进行修改,将主活动设置为LoginActivity而不是MainActivity:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.jojo.broadcastbestpractice">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
**<activity android:name=".LoginActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
intent-filter>
activity>
<activity android:name=".MainActivity">activity>**
application>
manifest>