Android四大组件之广播接收器(BroadCast Receiver)

(内容来自《Android第一行代码(第二版)》)

附:Android基础之四大组件

本文目录

1. 广播简介

2. 广播的接收

  • 2.1 动态注册监听网络变化
  • 2.2 静态实现开机启动注册

3. 发送自定义广播

  • 3.1 发送标准广播
  • 3.2 发送有序广播

4. 使用本地广播


分割线


1. 广播简介

Android中的广播主要可以分为两种类型:标准广播有序广播
标准广播( Normal broadcasts)是一种完全异步执行的广播,在广播发出之后,所有的广 播接收器几乎都会在同一时刻接收到这条广播消息,因此它们之间没有任何先后顺序可言。这种广播的效率会比较高,但同时也意味着它是无法被截断的。

标准广播的工作流程.png

有序广播( Ordered broadcasts)则是一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完毕后,广播才会继续传递。所以此时的广播接收器是有先后顺序的,优先级高的广播接收器就可以先收到广播消息,并且前面的广播接收器还可以截断正在传递的广播,这样后面的广播接收器就无法收到广播消息了。

有序广播的工作流程.png

2. 广播的接收

2.1.动态注册监听网络变化

广播接收器可以自由地对自己感兴趣的广播进行注册,这样当有相应的广播发出时,广播接收器就能够收到该广播,并在内部处理相应的逻辑。

注册广播的方式一般有两种,在代码中注册和在AndroidManifest.xml中注册,其中前者也被称为动态注册,后者也被称为静态注册。

那么该如何创建一个广播接收器呢?其实只需要新建一个类,让它继承自BroadcastReceiver,并重写父类的onReceive()方法就行了。
这样当有广播到来时, onReceive()方法就会得到执行,具体的逻辑就可以在这个方法中处理。

新建一个BroadcastTest项目,然后修改MainActivity中的代码
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) {
            ConnectivityManager connectionManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo networkInfo = connectionManager.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();
            }
        }
    }
}

可以看到,我们在 MainActivity中定义了一个内部类NetworkChangeReceiver,这个类是继承自 BroadcastReceiver的,并重写了父类的 onReceive()方法。这样每当网络状态发生变化时, onReceive()方法就会得到执行。

在 onReceive()方法中

  • 首先通过 getSystemService()方法得到了 ConnectivityManager的实例,这是一个系统服务类,专门用于管理网络连接的。
  • 然后调用它的 getActiveNetworkInfo()方法可以得到 NetworkInfo的实例
  • 接着调用 NetworkInfo的 isAvailable()方法,就可以判断出当前是否有网络了
  • 最后我们通过 Toast的方式对用户进行提示。

然后观察 onCreate()方法

  • 首先我们创建了一个 IntentFilter的实例,并给它添加了个值为 android.net.conn. CONNECTIVITY_CHANGE的 action,为什么要添加这个值呢?因为当网络状态发生变化时,系统发出的正是一条值为android.net.conn.CONNECTIVITY_CHANGE的广播,也就是说我们的广播接收器想要监听什么广播,就在这里添加相应的action。
  • 接下来创建了ー个 NetworkChangeReceiver的实例,然后调用registerReceiver()方法进行注册,将Network ChangeReceiver的实例和 IntentFilter的实例都传了进去,这样 NetworkChangeReceiver就会收到所有值为 android.net.conn.CONNECTIVITY_CHANGE的广播,也就实现了监听网络变化的功能。

最后要记得,动态注册的广播接收器一定都要取消注册才行,这里我们是在onDestroy()方法中通过调用 unregisterReceiver()方法来实现的。

另外,这里有非常重要的一点需要说明, Android系统为了保护用户设备的安全和隐私,做了严格的规定:如果程序需要进行一些对用户来说比较敏感的操作,就必须在配置文件中声明权限才可以,否则程序将会直接崩溃。比如这里访问系统的网络状态就是需要声明权限的。

打开AndroidManifest xml文件,在里面加入如下权限就可以访问系统网络状态了:

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

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

整体来说,代码还是非常简单的,现在运行一下程序。首先你会在注册完成的时候收到一条广播,然后按下Home键回到主界面(注意不能按Back建,否则 onDestroy()方法会执行),接着打开WLAN启动和禁用网络,你就会看到有 Toast提醒你网络发生了变化。


20190504_220455.gif

2.2.静态实现开机启动注册

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

这里我们准备让程序接收一条开机广播,当收到这条广播时就可以在onReceive()方法里执行相应的逻辑,从而实现开机启动的功能。可以使用 AndroidStudio提供的快捷方式来创建一个广播接收器


图片.png

右击 com. example. broadcasttest包→ New→ Other→Broadcast Receiver


图片.png

这里我们将广播接收器命名为 BootCompleteReceiver, Exported属性表示是否允许这个广播接收器接收本程序以外的广播, Enabled属性表示是否启用这个广播接收器。勾选这两个属性,点击 Finish完成创建。

然后修改 BootCompleteReceiver中的代码,如下所示

public class BootCompleteReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "Boot Complete", Toast.LENGTH_LONG).show();
    }
}

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

另外,静态的广播接收器一定要在AndroidManifest. xml文件中注册才可以使用,不过由于我们是使用 AndroidStudio的快捷方式创建的广播接收器,因此注册这一步已经被自动完成了。打开AndroidManifest.xml文件瞧一瞧,代码如下所示:

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

    <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">
       ...

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

</manifest>

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

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

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

    <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">
        ...
        <receiver
            android:name=".BootCompleteReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>
    </application>

</manifest>

由于 Android系统启动完成后会发出一条值为android.intent. action.BOOT_COMPLETED的广播,因此我们在< intent- filter>标签里添加了相应的 action。另外,监听系统开机广播也是需要声明权限的,可以看到,我们使用<uses-permission> 标签又加入了一条android.permission. RECEIVE_BOOT_COMPLETED权限。

现在重新运行程序后,我们的程序就已经可以接收开机广播了。将手机关闭并重新启动,在启动完成之后就会收到开机广播。

需要注意的是不要在onReceive()方法中添加过多的逻辑或者进行任何的耗时操作,因为在广播接收器中是不允许开启线程的,当onReceive()方法运行了较长时间而没有结束时,程序就会报错。因此广播接收器更多的是扮演一种打开程序其他组件的角色,比如创建一条状态兰通知,或者启动一个服务等。

3. 发送自定义广播

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();
    }
}

当MyBroadcastReceiver 收到自定义的广播时,就会弹出received in MyBroadcastReceiver的Toast

接下来在AndroidManifest.xml文件中对这个广播接收器进行修改:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.broadcasttest">

    <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=".BootCompleteReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>

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

    </application>

</manifest>

这里让MyBroadcastReceiver 接收一条值为com.example.broadcasttest.MY_BROADCAST的广播,因此待会我们需要发出这样一条广播。

修改activity_main.xml中的代码
<?xml version="1.0" encoding="utf-8"?>
<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/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Send Broadcast" />

</LinearLayout>

加入一个按钮,用来发送广播。

修改MainActivity代码

在onCreate()函数中加入按钮监听代码

        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v){
                Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
                sendBroadcast(intent);
            }
        });

首先构建Intent对象,并传入要发送的广播值,接着调用sendBroadcast()函数将广播发送出去,这样所有监听com.example.broadcasttest.MY_BROADCAST的广播接收器就会收到消息。

运行程序:


20190507_135937.gif

广播是一种可以跨进程的通信方式,这一点从前面的接收系统广播的时候就可以看出来。因此在我们应用程序内发出的广播,其他程序应该也是可以收到的。为了验证这一点,我们需要再创建一个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();
    }
}
在AndroidManifest.xml文件中对这个广播接收器进行修改
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.broadcasttest2">

    <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=".AnotherBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.example.broadcasttest.MY_BROADCAST" />
            </intent-filter>
        </receiver>
        
    </application>
</manifest>

这里接收的值和前面发送标准广播中的广播值一样
运行BroadcastTest2项目至手机,并在BroadcastTest项目中点击按钮,会发现分别弹出两次提示信息。


20190507_142819.gif

验证了我们的应用程序发出的广播是可以被其他的应用程序接收到的。
不过到目前为止,我们发出的广播都还是标准广播,接下来我们尝试一下有序广播。

3.2.发送有序广播

回到BroadcastTest项目

修改MainActivity中的代码
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v){
                Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
                sendOrderedBroadcast(intent,null);
            }
        });

这里我们只改动了一行代码,将sendBroadcast()方法改成了sendOrderedBroadcast()方法。
重新运行程序会发现两个程序还是会接收到这条广播的。不过这时的广播接收器是有先后顺序的,并且前面的广播接收器还可以将广播进行截断,阻止其继续传播。

至于广播接收器的先后顺序,这是在注册的时候进行设定的,修改AndroidManifest.xml文件


图片.png

这里通过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();
    }
}

调用abortBroadcast()用于将广播截断。

重新运行程序


20190507_150108.gif

只有MyBroadcastReceiver 中的Toast弹出,说明这条广播经过MyBroadcastReceiver 之后确实终止传递了。

4. 使用本地广播

前面所学的广播都是全局广播,这样有个问题就是容易引起安全问题,能不能使发出的广播只能在应用程序内传递而不被其他应用程序截获?这时就需要一个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.broadcasttest.LOCAL_BROADCAST");
                localBroadcastManager.sendBroadcast(intent); // 发送本地广播
            }
        });
        intentFilter = new IntentFilter();
        intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");
        localReceiver = new LocalReceiver();
        localBroadcastManager.registerReceiver(localReceiver, intentFilter); // 注册本地广播监听器
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        localBroadcastManager.unregisterReceiver(localReceiver);
    }

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

首先通过LocalBroadcastManager 的getInstance()方法获取一个实例
然后注册广播接收器的时候调用LocalBroadcastManager 的registerReceiver()方法
发送广播的时候调用的是LocalBroadcastManager 的sendBroadcast()方法

这里我们在按钮点击事件里发出一条com.example.broadcasttest.LOCAL_BROADCAST广播,然后在LocalReceiver 里接收这条广播。

重新运行程序


20190507_152625.gif

注:本地广播无法通过静态注册的方式来接收

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容