×

学习 | Android开发之详解广播机制

96
史慧君
2017.02.24 11:08* 字数 3123

案例最后效果:

广播的类型:

标准广播

标准广播是完全异步的广播, 也就是当广播发出之后, 所有的广播接收器机会都会在同一时刻接收到这条广播, 标准广播无法被拦截。

有序广播

有序广播是同步执行的广播, 也就是当广播发出之后, 同一时刻只会有一个广播接收器能够收到这条广播消息, 依次传递。优先级高的广播接收器可以优先接收广播消息, 同时前面的广播接收器还可以截取正在传递的广播, 这样后面的广播就无法收到广播消息了。

接收系统广播

Android内置了很多系统级别的广播, 比如:手机开机完成后会发出一条广播, 电池电量发生变化会发出一条广播, 时间或者时区发生变化会发出一条广播等等。如果希望接收这些广播, 就需要使用广播接收器, 下面来看具体用法。

动态注册监听网络变化

注册广播一般有两种方式:
动态注册:在代码中注册
静态注册:在AndroidManifest.xml中注册

首先我们来尝试一下动态注册:
只需要创建一个类BroadcaseDemo, 让它继承自Broadcast-Receiver,并重写父类的onReceive()方法就行了. onReceive()用来监听广播的到来, 具体逻辑也可以在这个方法中处理.

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private IntentFilter intentFilter;
    private NetWorkChangeReceiver networkChangeReceiver;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        intentFilter = new IntentFilter();
        intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
        networkChangeReceiver = new NetWorkChangeReceiver();
        registerReceiver(networkChangeReceiver, intentFilter);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(networkChangeReceiver);
    }
    class NetWorkChangeReceiver extends BroadcastReceiver{
        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(context, "network changes", Toast.LENGTH_SHORT).show();
        }
    }
}

说明: NetWorkChangeReceiver ()继承BroadcastReceiver, 并重写了父类的onReceive()方法. 这样每当网络发生变化的时候, onReceive()方法就会得到执行.

onCreate()方法, 我们首先创建一个IntentFilter的实例, 并未它添加了一个值为android.net.conn.CONNECTIVITY_CHANGE的action, 为什么要添加这个呢?因为当网络发生变化的时候, 系统发出的正是一条值为android.net.conn.CONNECTIVITY_CHANGE的广播. 也就是说我们的广播接收器想要监听什么广播, 就在这里添加相应的action.

接下来创建一个NetWorkChangeReceiver的实例, 然后调用registerReceiver()方法进行注册.

切记: 动态注册的广播接收器一定要取消注册才行, 这里我们是在onDestroy()方法中调用unregisterReceiver()方法来实现.

运行结果:


下面继续进行优化, 直接判断之后告诉用户, 当前环境是否有网.
MainActivity.java

  class NetWorkChangeReceiver extends BroadcastReceiver{
        @Override
        public void onReceive(Context context, Intent intent) {
            // 获取ConnectivityManager实例, 这是一个系统服务类, 专门用于管理网络连接
            ConnectivityManager connectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
            // 调用getActiveNetworkInfo()得到NetworkInfo的实例
            NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
            // 通过isAvailable()方法判断当前是否有网络
            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.junzaivip.broadcastdemo">
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
...
</manifest>
效果演示:
静态注册实现开机启动

动态注册的广播接收器可以自由的控制注册与注销, 在灵活性方面有很大的优势, 但是它必须在程序启动之后才能接收到广播, 因为注册的逻辑写在onCreate()方法中. 那么有没有什么办法可以让程序在未启动的情况下就能接收到广播呢? 这就需要使用静态注册的方式了.

准备实现一个开机启动的功能, 我们准备让程序接收一条开机广播, 当收到这条广播时, 就可以在onReceive()方法里执行相应的逻辑.

下面我们使用AS的快捷方式来创建一个广播接收器.

可以看到我们将广播接收器命名为BootCompleteReceiver, Exported属性表示是否允许这个广播接收器接收本程序以外的广播, Enabled属性表示是否启用这个广播接收器, 默认是勾选的, 然后点击finish完成创建.

修改完之后的代码如下:

public class BootCompleteReceive extends BroadcastReceiver {
    public BootCompleteReceive() {
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "Boot Complete", Toast.LENGTH_SHORT).show();
    }
}

代码非常简单, 我们只是在onReceive()方法中使用Toast弹出一段提示信息.

另外, 静态的广播接收器一定要在AndroidManifest.xml文件中注册才可以使用, 不过由于我们是使用的Android Studio的快捷方式创建的广播接收器, 因此注册这一步已经被自动完成了. 我们一起去看一看.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.junzaivip.broadcastdemo">

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <receiver
            android:name=".BootCompleteReceive"
            android:enabled="true"
            android:exported="true"></receiver>
    </application>

</manifest>

说明: 可以看到在</application>的标签内出现了一个新的标签<receiver>, 所有的静态广播接收器都是在这里进行注册的, 它的用法其实和<activity>标签非常相似, 也是通过android:name来指定具体哪一个广播接收器, 而android:enabled 和android:exported属性则是根据我们刚才勾选的状态自动生成的.

不过目前BootCompleteReceive还是不能接收到开机广播, 我们需要对AndroidManifest.xml文件进行修改才行, 如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.junzaivip.broadcastdemo">
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <receiver
            android:name=".BootCompleteReceive"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/>
            </intent-filter>
            
        </receiver>
    </application>

</manifest>

主要更新了下面的文件:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<intent-filter>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/>
</intent-filter>

说明: 由于Android系统启动完成后, 会发出android.permission.RECEIVE_BOOT_COMPLETED的广播, 因此我们在<intent-filter>标签里添加了相应的action. 另外监听系统开机广播也是需要声明权限的, 所以添加了一条<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>权限.

当然我们程序中只是简单使用了Toast提示了一段文本信息, 当你真正在项目中使用的时候, 就可以添加自己的逻辑.

需要注意的是, 不要在onReceive()方法中添加过多的逻辑或者进行任何的耗时操作, 因为在广播接收器中是不允许开启线程的. onReceive()方法中进行了较长时间而没有结束时, 程序就会报错.

广播接收器, 更多的是扮演一种打开程序或者其他组件的角色, 比如创建一条状态栏通知, 或者启动一个服务等.

发送自定义广播

前面的例子已经了解了接收系统广播, 接下来我们一起来学习在应用程序中发送自定义广播.

发送标准广播

首先定义一个广播接收器用来接收广播, 代码如下:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.junzaivip.broadcastdemo">

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <receiver
            android:name=".MyBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.junzaivip.broadcastdemo.MY_BROADCAST"/>
            </intent-filter>
        </receiver>
    </application>

</manifest>

可以看到, 这里让MyBroadcastReceiver接收一条值为com.junzaivip.broadcastdemo.MY_BROADCAST的广播, 因此待会儿在发送广播的时候, 就需要发出一条这样的广播.

接下来修改activity_main.xml中的代码, 如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:id="@+id/activity_main"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical">

   <Button
       android:id="@+id/send_cast"
       android:layout_width="match_parent"
       android:layout_height="wrap_content" 
       android:text="@string/send_broadcast"/>
</LinearLayout>

在布局中定义了一个按钮, 用于发送广播. 然后修改MainActivity中的代码.

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button sendButton = (Button) findViewById(R.id.send_cast);
        sendButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent("com.junzaivip.broadcastdemo.MY_BROADCAST");
                sendBroadcast(intent);
            }
        });
    }

说明: 按钮的点击事件里, 加入了自定义广播的逻辑. 首先构建出一个对象Intent对象, 并把要发送广播的值传入, 然后调用sendBroadcast()方法将广播发送出去, 这样所有监听com.junzaivip.broadcastdemo.MY_BROADCAST这条广播的广播接收器就会收到消息. 此时发出去的广播就是一条标准广播. 由于广播是通过Intent进行传递, 所以还可以在Intent中添加一些数据传递给广播接收器.

运行效果:

扩展 通过Intent来传递和接收数据方式:
通过intent来传递数据(字符串):

Intent intent = new Intent();
intent.setClass(DetailActivity.this, HistoryActivity.class);
intent.putExtra("","");
intent.putExtra("","");
intent.putExtra("","");
startActivity(intent);

通过intent来传递对象, 则在javabean需要实现序列化接口

import java.io.Serializable;
public class DetailModel implements Serializable{
}
Intent intent = new Intent();
intent.setClass(LeaderScheduleClientDetailActivity.this, LeaderScheduleContactHistoryActivity.class);
Bundle bundle = new Bundle();
bundle.putSerializable("contactHistorys",item);
intent.putExtras(bundle);
startActivity(intent);

接收intent传递过来的字符串:

true_exhibitionID = getIntent().getStringExtra("true_exhibitionID");

接收intent传递过来的对象

Bundle bundle = getIntent().getExtras();
model = (GroupModel) bundle.getSerializable("groupModel");

发送有序广播

因为广播是一种可以跨进程的通信方式, 因此我们应用程序内发出的广播, 其他的应用程序应该也是可以收到的.

下面我们就来一个例子, 通过第一个程序发送一个广播, 打开另外一个程序, 并且在第二个程序中显示自定义内容.

在另外一个程序中添加AnotherBroadcastReceiver, 代码如下:

public class AnotherBroadcastReceiver extends BroadcastReceiver {
    public AnotherBroadcastReceiver() {
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        Intent i = new Intent();
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        i.setClass(context,MainActivity.class);
        context.startActivity(i);
        Toast.makeText(context, "另外一个广播", Toast.LENGTH_SHORT).show();
    }
}

在AndroidManifest.xml中配置:

 <receiver
            android:name=".AnotherBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.junzaivip.broadcastdemo.MY_BROADCAST"/>
            </intent-filter>
</receiver>

说明: Intent i = new Intent();
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
i.setClass(context,MainActivity.class);
context.startActivity(i); 这一段就是核心代码, 用来打开另外一个程序.

这样就可以通过广播打开另外一个应用程序了, 也可以显示内容. 下面是效果演示:

这样就强有力的证明了, 我们的应用程序发出的广播是可以被其他应用程序接收到的.

不过到目前为止, 程序里发出的广播都还是标准广播, 现在我们来尝试一下发送有序广播.
重新回到broadcastDemo项目, 然后修改MainActivity中的代码, 如下:

将 sendBroadcast(intent); 修改为 sendOrderedBroadcast(intent,null);

可以看到发送有序广播只需要改动一行代码, 即将sendBroadcast()方法改成sendOrderedBroadcast()方法.

sendOrderedBroadcast()方法接收两个参数, 第一个参数仍然是Intent, 第二个参数是一个与权限相关的字符串, 这里传入null就行了. 现在重新运行程序, 并点击Send Broadcast按钮, 你会发现两个应用程序都还是可以接收到这条广播.

看上去貌似没什么区别, 这个时候广播接收器是有先后顺序的. 而且前面的广播接收器还可以将广播截断, 以阻止其继续传播.

通过android:priority属性给广播接收器设置优先级, 优先级比较高的广播就可以先收到广播. 下面给它设置了100, 以保证它一定会会在另一个之前收到广播. 同时它也有权限来决定是否允许广播继续传递.
AndroidManifest.xml

 <receiver
            android:name=".MyBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter android:priority="100">
                <action android:name="com.junzaivip.broadcastdemo.MY_BROADCAST"/>
            </intent-filter>
        </receiver>

下面我们来配置让第一个应用程序阻止继续传递.


public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "欢迎关注公众号:史慧君, 测试接收一个自定义广播", Toast.LENGTH_SHORT).show();
        abortBroadcast();
    }
}

说明:
abortBroadcast();表示将这条广播阶段, 后面的广播接收器将无法再继续接收这条广播.

使用本地广播

前面我们发送和接收的广播全部属于系统全局广播, 即发出的广播可以被其他任何应用程序接收到, 并且我们也可以接收来自其他任何应用程序的广播. 这样就很容易引起安全感的问题. 比如我们发送一条带有关键性数据的广播就可能被其他的应用程序截获, 或者其他的应用程序不停的向我们的广播接收器里发送各种垃圾广播.

为了能简单解决广播这个安全问题, Android引入了一套本地广播机制, 使用这个机制发出的广播只能够在应用程序的内部进行传递, 并且广播接收器也只能接收来自本应用程序发出的广播, 这样所有的安全问题, 就不存在了.

本地广播的用法并不复杂, 主要就是使用了一个LocalBroadcastManager, 来对广播进行管理, 并提供发送广播和注册广播接收器的方法. 下面我们通过具体的实例来尝试他的用法.

修改MainActivity.java中的代码:

public class MainActivity extends AppCompatActivity {

    private IntentFilter intentFilter;
    private NetWorkChangeReceiver networkChangeReceiver;
    private LocalReveiver localReceiver;
    private LocalBroadcastManager localBroadcastManager;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        localBroadcastManager = LocalBroadcastManager.getInstance(this); //获取实例
        Button sendButton = (Button) findViewById(R.id.send_cast);
        sendButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent("com.junzaivip.broadcasttest.LOCAL_BORADCAST");
                localBroadcastManager.sendBroadcast(intent);//发送本地广播
            }
        });


        intentFilter = new IntentFilter();
        intentFilter.addAction("com.junzaivip.broadcasttest.LOCAL_BORADCAST");
        localReceiver = new LocalReveiver();
        localBroadcastManager.registerReceiver(localReceiver, intentFilter); //注册广播监听器


    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(networkChangeReceiver);
    }
    class NetWorkChangeReceiver extends BroadcastReceiver{
        @Override
        public void onReceive(Context context, Intent intent) {
            // 获取ConnectivityManager实例, 这是一个系统服务类, 专门用于管理网络连接
            ConnectivityManager connectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
            // 调用getActiveNetworkInfo()得到NetworkInfo的实例
            NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
            // 通过isAvailable()方法判断当前是否有网络
            if(networkInfo!= null && networkInfo.isAvailable()){
                Toast.makeText(context, "网络可用", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(context, "网络不可用", Toast.LENGTH_SHORT).show();
            }
        }
    }


    class  LocalReveiver extends BroadcastReceiver{
        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(context, "reveived local broadcast", Toast.LENGTH_SHORT).show();
        }
    }
}

说明: LocalBroadcastManager.getInstance()方法得到了它的一个实例, 注册广播接收器的时候调用的是LocalBroadcastManager的registerReceiver()方法.
发送广播的时候调用的是localBroadcastManager.sendBroadcast()方法.

运行效果

需要说明: 本地广播无法通过静态注册的方式来接收. 其实这也完全可以理解. 因为静态注册主要就是为了让程序在未启动的情况下也能进行广播, 而发送本地广播时, 我们的程序肯定是已经启动了的. 因此也完全不需要使用静态注册的方法.

使用本地广播的几点优势:
可以明确的知道正在发送的广播不会离开我们的程序, 因此不必担心机密数据泄露.

其他程序无法将广播发送到我们程序内部, 因此不需要担心会有安全漏洞的隐患.

发送本地广播比发送系统全局广播会更加有效.

技术成长
Web note ad 1