Android基础回顾(四)| 关于广播机制

参考书籍:《第一行代码》 第二版 郭霖
如有错漏,请批评指出!

广播机制简介

Android中的广播主要分两种类型:标准广播和有序广播

  • 标准广播:一种完全异步执行的广播,在广播发出之后,所有的广播接收器几乎都会在同一时刻接收到这条广播信息,没有先后顺序可言。这种广播效率比较高,但是无法截断。

  • 有序广播:一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完毕后广播才会继续传递。此时广播接收器有先后顺序,优先级高的广播接收器可以先收到广播消息,并且前面的广播接收器可以截断广播。

接收系统广播

Android内置了很多系统级别的广播,我们可以在应用程序中通过监听这些广播来得到各种系统的状态信息。使用广播接收器可以自由地对需要的广播进行注册,当有相应广播发出时,广播接收器就能够收到该广播,并对其进行逻辑处理。
注册广播的方式一般有两种:一是动态注册,即在代码中注册;二是静态注册,即在AndroidManifest文件中注册。
创建广播接收器的方法是新建一个类,继承BroadcastReceiver类,并重写onReceive()方法,当有广播到来时,onReceive()方法会被调用,我们可以在onReceive()方法中添加具体逻辑。

  • 动态注册监听网络变化
    下面我们使用动态注册的方式来完成一个监听网络变化的demo:
    首先,在AndroidManifest文件中声明系统网络状态的权限,因为我们要监听系统网络状态的变化,就必须拥有这个权限:

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

    接下来修改BroadcastActivity中的代码:

    public class BroadcastActivity extends AppCompatActivity {
    
        private NetworkChangeReceiver networkChangeReceiver;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            initBroadCast();
        }
    
        private void initBroadCast() {
            IntentFilter 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 manager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
                NetworkInfo networkInfo = manager.getActiveNetworkInfo();
                if (networkInfo != null && networkInfo.isConnected()) {
                    ToastUtil.showShortToast(context, "Network is connected");
                }else {
                    ToastUtil.showShortToast(context, "Network not connected");
                }
            }
        }
    }
    
    1. 第一步,创建一个内部类NetworkChangeReceiver,继承BroadcastReceiver类,重写它的onReceive()方法。在onReceive()方法中通过getSystemService()方法得到一个ConnectivityManager的实例,这是一个系统服务类,专门用于管理网络连接的。然后调用它的getActiveNetworkInfo()方法可以得到NetworkInfo的实例,接着调用NetworkInfo的isConnected()方法,就可以判断出当前是否有网络了。当然,想要onReceive()方法被触发,我们需要对网络状态变化时发出的系统广播进行注册。
    2. 第二步,因为系统网络状态发生变化时,会发出一条值为
      android.net.conn.CONNECTIVITY_CHANGE 的广播,所以我们创建一个IntentFilter实例,并给它添加值为 android.net.conn.CONNECTIVITY_CHANGE 的action(也就是说,我们想要监听什么广播,就添加其对应的action)。
    3. 第三步,创建一个NetworkChangeReceiver的实例,然后调用 registerReceiver() 方法进行注册,将NetworkChangeReceiver的实例和IntentFilter的实例传进去,这样就完成了注册,也就是说,当系统发出值为android.net.conn.CONNECTIVITY_CHANGE的广播时,我们的onReceive()方法就会被触发。
    4. 第四步,重写onDestroy()方法,调用unregisterReceiver()方法取消注册。这里要注意,我们动态注册的广播都需要取消注册。下面看效果(打开我们的Demo,然后切到网络设置的页面,注意,不要按back键退出demo,不然BroadcastActivity会被销毁):
  • 静态注册实现开机启动
    动态注册的广播接收器可以自由地控制注册与注销,在灵活性上有很大的优势,但是它也有自己的局限性,即必须在程序启动后才能接收到广播,因为注册逻辑是写在onCreate()方法中的。这时候,静态注册的方式就有用武之地了。接下来,我们通过静态注册的方式来接收一条开机广播。

    1. 第一步,新建一个BootCompleteReceiver类,继承BroadcastReceiver类,并重写其onReceive()方法,使用Toast弹出一条消息:
    public class BootCompleteReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            ToastUtil.showShortToast(context, "Boot Completed");
        }
    }
    
    1. 第二步,在AndroidManifest文件中注册广播:
    <receiver
        android:name=".broadcast.BootCompleteReceiver"
        android:enabled="true"
        android:exported="true" >
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED"/>
        </intent-filter>
    </receiver>
    

    通过android:name属性指定我们创建的广播接收器(完整路径);android:enabled
    属性定义系统是否能够实例化这个广播接收器,为true时这个广播接收器才能被启用,默认为true;android:exported属性用于指示该广播接收器是否能够接收来自应用程序外部的消息。然后在<intent-filter>标签中添加系统开机广播对应的action,这和动态注册创建Intentfilter实例并添加action的作用是一样的。

    1. 第三步,由于我们需要监听系统开机广播,因此也需要为其声明权限,在AndroidManifest文件中声明权限:
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    

这样,我们的静态注册广播就完成了,具体结果自行验证。
注意:在onReceive()方法中不能进行耗时操作,因为广播接收器中不允许开启线程,若onReceive()方法运行较长时间,程序就会报错。

自定义广播

前面是关于如何使用广播接收器来接收系统广播,接下来我们来看看如何发送自定义广播。

  • 发送标准广播
    首先来新建一个DiyBroadcastReceiver类,继承BroadcastReceiver类,重写onReceive()方法:

    public class DiyBroadcastReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            ToastUtil.showShortToast(context, "received in my broadcast receiver");
            abortBroadcast();
        }
    }
    

    然后在AndroidManifest文件中对这个广播接收器进行注册:

    <receiver android:name=".broadcast.broadcast.DiyBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter android:priority="13">
                <action android:name="com.laughter.broadcast.DIY_BROADCAST"/>
            </intent-filter>
        </receiver>
    

    我们添加一条值为 com.laughter.broadcast.DIY_BROADCAST 的action,也就意味着,我们待会儿发出一条值为这个的广播,我们的广播接收器就能收到。
    接下来,在我们的布局文件中添加一个Button,用于触发发送广播(很简单,我就不贴代码了),然后修改BroadcastActivity中的代码:

    public class BroadcastActivity extends AppCompatActivity {
    
        @BindView(R.id.but_send)
        Button send;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_broadcast);
            ButterKnife.bind(this);
        }
    
        @OnClick(R.id.but_send)
        public void send() {
            Intent intent = new Intent("com.laughter.broadcast.DIY_BROADCAST");
            sendBroadcast(intent);
        }
    }
    

    发送广播的方法很简单,就是创建一个Intent对象,将需要发送的广播的值作为参数传递进去,然后调用sendBroadcast()方法将广播发送出去,这样监听 com.laughter.broadcast.DIY_BROADCAST 这个值的广播接收器就能收到这条广播(这里就不贴效果图了,大家可以自己验证)。由于是用Intent发送广播,因此,还可以用这个Intent对象携带一些参数。
    广播是一种可以跨进程的通信方式,因此,我们在应用程序中发送的广播,别的应用程序也是可以收到的。下面我们来验证一下:
    首先创建一个BroadcastTest项目,然后定义一个广播接收器:

    public class AnotherReceiver extends BroadcastReceiver {
    
        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(context, "received in another receiver", Toast.LENGTH_SHORT).show();
        }
    }
    

    在AndroidManifest文件中进行注册:

    <receiver
        android:name=".AnotherReceiver"
        android:enabled="true"
        android:exported="true">
        <intent-filter>
            <action android:name="com.laughter.broadcast.DIY_BROADCAST"/>
        </intent-filter>
    </receiver>
    

    这样,我们的两个程序就都能说收到这条广播了,下面将两个app都运行起来,验证一下:

    可以看到,当我们点击Button时,两条Toast都弹出了, 也就意味着,两个app中的广播接收器都收到了这条广播。

  • 发送有序广播
    要发送一条有序广播,我们只需要在前面的BroadcastActivity中,将sendBroadcast(intent) 方法换成 sendOrderedBroadcast(intent, null) 方法就行了(第一个参数还是Intent对象,第二个参数是与权限相关的字符串,这里直接传null就行):

    public class BroadcastActivity extends AppCompatActivity {
    
        ···
    
        @OnClick(R.id.but_send)
        public void send() {
            Intent intent = new Intent("com.laughter.broadcast.DIY_BROADCAST");
            sendOrderedBroadcast(intent, null);
        }
    }
    

    前面说过,发送有序广播的时候,广播接收器之间存在优先级,那么如何指定优先级呢?

    <receiver android:name=".broadcast.broadcast.DiyBroadcastReceiver"
        android:enabled="true"
        android:exported="true">
        <intent-filter android:priority="13">
            <action android:name="com.laughter.broadcast.DIY_BROADCAST"/>
        </intent-filter>
    </receiver>
    

    我们只需要在注册广播接收器的时候给<intent-filter>标签指定一个android:priority 属性就行了,里面的值就是优先级,值越大,优先级越高。同样的,给AnotherReceiver指定一个优先级,数字比这里的13小就行。这样,优先级高的广播接收器就会先收到广播。(这里也不贴图了,自行验证)
    前面还提到过,有序广播是可以被拦截的,如何拦截呢?我们只需要在广播接收器的onReceive()方法中调用 abortBroadcast(); 方法就可以拦截广播,这样优先级比这个广播接收器低的就收不到这条广播了。

    public class DiyBroadcastReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            ToastUtil.showShortToast(context, "received in my broadcast receiver");
            abortBroadcast();
        }
    }
    

    还是刚才的两个app,我们再来运行起来看看:

    可以看到,现在只有一个优先级高的广播接收器的onReceive()方法被触发,弹出了Toast,而由于广播被拦截,优先级低的AnotherReceiver没有收到广播,所以没有弹出Toast。

使用本地广播

前面我们发送和接收的广播全部属于系统全局广播,即发出的广播可以被任何其他的应用程序接收到,并且也可以接受其他任何应用程序的广播。这样就很容易引起安全性问题。为此,Android引入了一套本地广播机制,使用本地广播机制,广播只能在应用程序内部进行传递,并且广播接收器也只会接收到应用程序内部的广播。
其实本地广播的用法和前面差不多,我们先来看代码:

public class BroadcastActivity extends AppCompatActivity {

    @BindView(R.id.but_send)
    Button send;

    LocalBroadcastManager manager;
    LocalReceiver localReceiver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_broadcast);
        ButterKnife.bind(this);
        initView();
    }

    private void initView() {
        manager = LocalBroadcastManager.getInstance(this);
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("com.laughter.broadcast.LOCAL_BROADCAST");
        localReceiver = new LocalReceiver();
        manager.registerReceiver(localReceiver, intentFilter);
    }

    @OnClick(R.id.but_send)
    public void send() {
        Intent intent = new Intent("com.laughter.broadcast.LOCAL_BROADCAST");
        manager.sendBroadcast(intent);
    }

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

    class LocalReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            ToastUtil.showShortToast(context, "received local broadcast");
        }
    }
}

和前面一样,我们首先定义一个内部类,即广播接收器,然后定义一个 LocalBroadcastManager 对象,通过它的 getInstance() 方法获取一个实例,接下来就是定义IntentFilter对象和广播接收器对象,给IntentFilter添加一条值为 com.laughter.broadcast.LOCAL_BROADCAST 的action,然后调用 LocalBroadcastManager 的 registerReceiver() 方法注册本地广播监听器,接下来在Button的点击事件中通过 LocalBroadcastManager 的 sendBroadcast() 方法发送广播,最后别忘了在onDestroy() 方法中取消注册。这样看起来,本地广播区别于全局广播的地方仅仅就是通过一个LocalBroadcastmanager 来管理广播的注册和发送。这样我们的广播就是仅仅在应用程序内部传递的了(感兴趣的自行验证)。
注意:既然需要通过 LocalBroadcastmanager 来管理广播的注册和发送,那么对应的静态注册的方式怎么实现呢?很遗憾,本地广播是无法通过静态注册的方式来接收的。不过仔细想想,本地广播的发送很显然是需要在应用程序启动的情况下完成的,既然要在应用程序启动的情况下发送广播,那么也不用考虑在应用程序未启动的情况下接收广播了。而我们使用静态注册主要就是为了在应用程序未启动的情况下也能收到广播,所以思路就很清晰了。


上一篇:Android基础回顾(三)| 关于Fragment
下一篇:Android基础回顾(五)| 数据存储——持久化技术


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

推荐阅读更多精彩内容