Android学习笔记——广播机制

来源:互联网 发布:mac find 查找文件夹 编辑:程序博客网 时间:2024/05/02 00:24

参考书籍: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信息。
这里写图片描述

(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<Activity> 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>

运行程序。
这里写图片描述这里写图片描述这里写图片描述

原创粉丝点击