Android学习笔记——广播机制

参考书籍: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);//发送标准广播
            }
        });**
        。。。

运行程序,点击按钮会弹出Toast信息。
Android学习笔记——广播机制_第1张图片

(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项目主界面,点击按钮,会分别弹出两次提示信息。
Android学习笔记——广播机制_第2张图片Android学习笔记——广播机制_第3张图片
由此可见,应用程序发出的广播可以被其他应用程序接收到。

发送有序广播:
修改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消息:
Android学习笔记——广播机制_第4张图片
这时如果想在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>

运行程序。
Android学习笔记——广播机制_第5张图片Android学习笔记——广播机制_第6张图片Android学习笔记——广播机制_第7张图片

你可能感兴趣的:(Android学习笔记)