Android 面试(三):用广播 BroadcastReceiver 更新 UI 界面真的好吗?

这是 面试系列 的第三期。本期我们将来探讨一下 Android 四大组件的重要组成部分:广播 BroadcastReceiver。

往期内容传递:
Android 面试:说说 Android 的四种启动模式
Android 面试:如何理解 Activity 的生命周期

前言

BroadcastReceiver 作为 Android 四大组件之一,应用场景可谓非常之多。所以我相信任何一个有一定 Android 开发经验的工程师都不会在这个题上栽跟斗。但,某些细节,或许我们可以注意一下。

实际上我在面试过程中也遇到了这样的题。下面请允许我用「柳学兄」的思路带大家进入面试营。

BroadcastReceiver 内部基本原理是什么?

Android 的广播 BroadcastReceiver 是一个全局的监听器,主要用于监听 / 接收应用发出的广播消息,并作出响应。其采用了设计模式中的 观察者模式 ,可将广播基于 消息订阅者消息发布者消息中心(AMS:即 Activity Manager Service)解耦,通过 Binder 机制形成订阅关系。

图片来源于网络

说说 BroadcastReceiver 的两种注册方式

Android 广播的两种注册方式肯定难不倒任何人,实际上我估计也只有对少量的 Android 开发面试者才会遇到这样的题,这里不会有什么特别的,熟悉的可以直接跳过

  • 静态注册
    静态注册广播的方式只需要在 AndroidManifest.xml 里通过 <receiver> 标签声明。下面附上一些属性说明。
<receiver 
    android:enabled=["true" | "false"]
    //此 broadcastReceiver 能否接收其他 App 发出的广播
    //默认值是由 receiver 中有无 intent-filter 决定的:如果有 intent-filter,默认值为 true,否则为 false
    android:exported=["true" | "false"]
    android:icon="drawable resource"
    android:label="string resource"
    //继承 BroadcastReceiver 子类的类名
    android:name=".mBroadcastReceiver"
    //具有相应权限的广播发送者发送的广播才能被此 BroadcastReceiver 所接收;
    android:permission="string"
    // BroadcastReceiver 运行所处的进程
    // 默认为 App 的进程,可以指定独立的进程
    //注:Android 四大基本组件都可以通过此属性指定自己的独立进程
    android:process="string" >

    //用于指定此广播接收器将接收的广播类型
    //本示例中给出的是用于接收网络状态改变时发出的广播
     <intent-filter>
          <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
    </intent-filter>
</receiver>
  • 动态注册
    动态注册方式是通过调用 Context 下面的 registerReceiver() 进行注册,可以调用 unregisterReceiver() 进行注销。需要注意的是:动态广播最好在 Activity 的 onResume() 注册,并在 onPause() 进行注销。

为什么建议动态广播尽量在 onPause() 进行注销?

我们可以先看看 Activity 的生命周期。

图片来源于网络

首先有注册就得有注销,否则一定会造成内存泄漏。注意上面途中红框圈住的部分。,阅读官方源码发现,当系统因为内存不足需要回收 Activity 占用的资源时,Activity 在执行完 onPause() 方法后就可能面临着被销毁的危险,有些生命周期方法,如:onStop()onDestroy() 根本就不会执行,而 onPause() 由于一定会调用的特殊性,自然是避免内存泄漏的好方法。

两种注册方式的区别也是可以用图一目了然。


图片来源于网络

说说 Android 的常用广播类型吧

基本在 Android 领域常用的方式就是直接调用 Context 提供的方法 sendBroadcast()sendOrderBroadcase() 发送无序广播和有序广播。

  • 无序广播
    无序广播是完全异步的,通过 Context.sendBroadcast() 方法来发送,从效率上来看,还算是比较高的。正如它的名称一样,无序广播对所有的广播接收者而言,是无序的。也就是说,所有接收者无法确定接收时序的顺序,这样也导致了,无序广播无法被停止。当它被发送出去之后,它将通知所有这条广播的接收者,直到没有与之匹配的广播接收者为止。

  • 有序广播
    有序广播通过 Context.sendOrderedBroadcast() 方法来发送。有序广播和无序广播最大的不同,就是它可以允许接收者设定优先级,它会按照接收者设定的优先级依次传播。而高优先级的接收者,可以对广播的数据进行处理或者停止掉此条广播的继续传播。广播会先发送给优先级高 (android:priority) 的 Receiver,而且这个 Receiver 有权决定是继续发送到下一个 Receiver 或者是直接终止广播。

除了无序广播和有序广播,还有其他的类型吗?

可能还是有不少的朋友知道 Sticky 广播方式。

  • 粘性广播 Sticky
    Sticky 广播和它的名字很像,它是一个具有粘性的广播。它被发出去之后,会一直滞留在系统中,直到有与之匹配的接收者,才会将其发出去。它采用 Context.sendStickyBroadcast() 方法进行发送广播。

    从官方文档上可以看到,如果想要发送一个 Sticky 广播,需要具有 BROADCAST_STICKY 权限,这个可以在 AndroidManifest.xml 中进行注册,而如果没有此权限,则会抛出 SecurityException 异常。

    对于系统而言,只会保留最后一条 Sticky 广播,并且会一直保留下去,也就是说,如果我们发送的 Sticky 广播不被取消,当有一个接收者的时候就会收到它,再来一个还是能收到。所有我们需要在合适的实际,调用 removeStickyBoradcast() 方法,将其取消掉。

    从官方文档中也可以看到 StickyBroadcast 已经被标记为 @Deprecated ,出于一些安全的考虑,已经将其标记为废弃,不再推荐使用。我们作为开发者,对于一些被标记为 @Depracated 的方法,使用起来还是需要谨慎的。

有时候基于数据安全考虑,我们想发送广播只有自己(本进程)能接收到,怎么处理?

首先,Android 中的广播可以跨进程通信,因为 exported 对于有 Intent-filter 的情况下默认为 true。所以我们难以有这样的需求:

  • 对于某些敏感性的广播,我们不希望暴露给外部。
  • 其他 App 可能会发出和当前 App intent-filter 相匹配的广播,导致 App 不断进行广播接收和处理。

这真是一个坏消息,我们必须让我们的应用变得有效率并足够的安全

一般我们能自然地想到在注册广播的时候把 exported 值设为 false 并给 App 的广播增加上权限,可问题是权限不够是一个字符串,面对当前如此强大的反编译技术,这终究是不安全的。

为了解决这样的问题,我们不难想到可以通过往主线程的消息池(Message Queue)里发送消息,让其做到只有主线程的 Handler 可以分发处理它。或者在发送广播的时候直接通过 Intent.setPackage(packageName) 指定广播接收器的包名。

要不是我们项目中有个 BroadcastUtil 工具类,我还之前真不知道 Support V4 包下还有这么一个 LocalBroadcastManager 本地广播类。

本地广播 在 Android Support v4 : 21 版本后加入了我们的大家庭。它使用 LocalBroadcastManager (以下简称 LBM)类来管理。

LocalBroadcast 的使用非常的简单,只需要将 Broadcast 的对应 API,替换为 LBM 为我们提供的 API 即可。

LBM 是一个单例对象,可以使用 LocalBroadcastManager.getInstance(Context context) 方法获取到。在 Context 中定义的和 Broadcast 相关的方法,在 LBM 中都有对应的 API 。非常有意思的是,LBM 为了区分异步和同步,使用了 sendBroadcast()sendBroadcastSync() 方法来做为区分。

在 Android 中用广播来更新 UI 界面好吗?

废话扯了这么多,终于说到标题上的问题了。

直接回答:可以,为什么不可以呢?在实际开发中我们不是经常这么用么?

很好,可以肯定你是一个真实的 Android 开发者了,不过在认证你的「合格」之前,想问问 BroadcastReceiver 的生命周期。

什么?BroadcastReceiver 的生命周期?糟糕,面试前只复习了 Activity 和 Fragment 的生命周期,杂还有人问 BroadcastReceiver 的生命周期。

所以,你支支吾吾了。

其实还是有比较多的人了解 BroadcastReceiver 的生命周期的。BroadcastReceiver 有生命周期,但比较短,而且很短。当它的 onReceive() 方法执行完成后,它的生命周期也就随之结束了。这时候由于 BroadcastReceiver 已经不处于 active 状态,所以极有可能被系统干掉。也就是说如果你在 onReceive() 去开线程进行异步操作或者打开 Dialog 都有可能在没达到你要的结果时进程就被系统杀掉了。

所以,正确答案是?

更新 UI 界面这个定义太广泛了。实际开发中其实大多数情况都是可以采用 BroadcastReceiver 来更新 UI,所以也造成了很多人回答就想上面很肯定和自信的回答可以。

实际上我们知道 Receiver 也是运行在主线程的,不能做耗时操作。虽然超时时间相对于 Activity 的 5 秒更高,有足足的 10 秒。但不意味着我们实际开发中所有的更新 UI 界面操作时间都在安全范围之内。

此外,对于频繁更新 UI,也不推荐这种方式。Android 广播的发送和接收都包含了一定的代价,它的传输都是通过 Binder 进程间通信机制来实现的,那么系统肯定会为了广播能顺利传递而做一些进程间通信的准备。而且可能会由于其它因素导致广播发送和到达不准时(或者说接收会延迟)。

这种情况可能吗?

很可能,而且很容易发生。我们要先了解 Android 的 ActivityManagerService 有一个专门的消息队列来接收发送出来的广播,sendBroadcast() 执行完后就立即返回,但这时发送来的广播只是被放入到队列,并不一定马上被处理。当处理到当前广播时,又会把这个广播分发给注册的广播接收分发器ReceiverDispatcher,ReceiverDispatcher 最后又把广播交给接 Receiver 所在的线程的消息队列去处理(就是你熟悉的 UI 线程的 Message Queue)。

整个过程从发送 ActivityManagerService 到 ReceiverDispatcher 进行了两次 Binder 进程间通信,最后还要交到 UI 的消息队列,如果基中有一个消息的处理阻塞了 UI,当然也会延迟你的 onReceive() 的执行。

BroadcastReceiver 和 EventBus 有啥不同?

EventBus 作为 GitHub 上一个颇受欢迎的库,目前也是有着 16.3 k 的星星,足以见其强大。

所以在不少面试中当然会遇到这样的提问。这不,笔者在咕咚面试的时候就被面试官问到了这个题,又一个打脸,当时我像被电了一番,答的并不怎么样。

众所周知,广播是 Android 的四大组件之一。系统系统级的事件都是通过广播来通知的,比如说网络的变化、电量的变化、短信接收和发送状态等。所以,如果是和 Android 系统相关的通知,我们还得选择本地广播。

但是!!!广播相对于其他实现方式,是很重量级的,它消耗的资源较多。它的优势体现在和 SDK 的紧密联系,onReceive() 方法自带了 Context 和 Intent 参数,所以在一定意义上实现了便捷性,但如果对 Context 和 Intent 应用很少或者说只做很少的交互的话,使用广播真的就是一种浪费!!!

那 EventBus 呢?

先说说其优点:

  • 调度灵活
    要说到优点,这一定是我最先想到的。因为它真的是太灵活了,在实际开发中感觉它就是一个机灵鬼,想去哪就去哪,根本就不需要像广播一样关注 Context 的注入与传递。父类对于通知的监听和处理还可以直接继承给子类,可以设置优先级让 Subscriber 关注到优先级更高的通知,其粘滞事件(sticky events)能够保证通知不会因 Subscriber 的不在场而忽略。可继承、优先级、粘滞,是 EventBus 比之于广播、观察者等方式最大的优点,它们使得创建结构良好组织紧密的通知系统成为可能。

  • 使用简单
    进入到 EventBus 的官网,看一眼 README.md,简直不能再简单,简简单单三个步骤,再在 build.gradle 中添加一个依赖,轻轻松松搞定有木有?如果不想创建 EventBus 的实例,还可以直接调用静态方法 EventBus.getDefault() 获取。

  • 快速且轻量
    作为一个 GitHub 的明星项目,性能方面是可以放心的。

EventBus 这么棒,那我们有组建通信就用 EventBus 吧。

还真是人无完人,物无完物。EventBus 也有着它的致命弱点。EventBus 最大的缺点在于其逻辑性,直接看其代码,一不小心根本看不通有没有?另外一个问题是,当程序较大后,观察者独有的接口膨胀缺点也会伴随着你的项目,你能想象很多 Event 后缀类的感觉吗?

综上,EventBus 由于其针对统一进程,所以在某些复杂的情况下单纯依靠接口回调不好处理组件通信的时候,直接去尝试 EventBus 吧。

说了这么多,在广播和 EventBus 这个十字路口犹豫不决的时候,还会纠结选择吗?

做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~


nanchen
上一篇:Android 面试(一):说说 Activity 的四种启动模式


下一篇:如何解决 Android Studio 三方库依赖冲突问题