安卓原生-广播
前言
- 我们上学时都有过这样的经历,当我们在火车站列车候车室中等待时,每当有某次列车开始检票或者进站上车时,就会播放通知来告知在候车室等待的人们该消息。
- 为了便于进行系统级别的消息通知,Android引入了一套类似的广播机制,然而比上述情景要灵活得多。此文将对Android广播机制的方方面面做出详尽的介绍。
Android广播机制简介
前面我们提到,Android的广播机制更加的灵活,这是因为Android允许每个应用只对自己感兴趣的广播进行注册,这样该程序就只会收到自己所关心的广播内容。
Android广播分为两个方面:广播发送者和广播接收者,通常情况下,BroadcastReceiver
指的就是广播接收者(广播接收器)。
广播机制最大的特点就是发送方并不关心接收方是否接到数据,也不关心接收方是如何处理数据的。
前半句,有点类似于UDP
Android中广播的是操作系统中产生的各种各样的事件。例如,收到一条短信就会产生一个收到短信息的事件。而Android操作系统一旦内部产生了这些事件,就会向所有的广播接收器对象来广播这些事件。
BraodcastReceiver(广播接收器)是为了实现系统广播而提供的一种组件,并且广播事件处理机制是系统级别的。比如,我们可以发出一种广播来测试是否收到短信,这时候就可以定义一个BraodcastReceiver来接受广播,当收到短信时提示用户。我们既可以用Intent来启动一个组件,也可以用sendBroadcast()方法发起一个系统级别的事件广播来传递消息。
我们也可以在自己的应用程序中开发BroadcastReceiver,然后把广播接收器这个类或者对象注册到Android操作系统上去,让操作系统知道现在有这样一个广播接收器正在等待接收Android操作系统的广播,即在自己的应用程序中实现BroadcastReceiver来监听和响应广播的Intent。
当有广播事件产生时,Android操作系统首先告诉注册到其上面的广播接收器产生了一个怎么样的事件,每个接收器首先判断是不是我这个接收器需要的事件,如果是它所需要的事件,再进行相应的处理。
例子,我们把骚扰电话的黑名单放到数据库中去,当接到电话时会产生一个接电话事件,事先在Android操作系统中注册一个BroadcastReceiver的对象,当产生事件的时候,会通知我们的广播接收器对象,接收器对象接收到消息之后,就会到数据库里面去取所有黑名单电话和接到的这个电话号码进行比较,如果匹配就直接挂掉。
应用场景
- 同一应用具有多个进程的不同组件之间的消息通信
- 不同应用间的组件之间的消息通信
- 与Android系统在特定情况下的通信
- 如:系统开机,网络变化等
以上只说明适合广播机制的应用场景,还有一些场景理论上可以使用,但是实际开发没有人这么做:
- 同一应用内同一组件的消息通信:显然扩展变量的作用域、接口回调、
Handler-Message
等方式都能更简单的实现。- 同一应用内的不同组件之间的消息通信(单个进程):对于简单的的情况,依靠接口的回调方式就可解决;而较为复杂的情况,更推荐直接使用
EventBus
等。
实现原理
设计模式与模型
Android中的广播使用了观察者模式,模型为基于消息的发布/订阅事件模型。
从设计模式上讲,广播的发送者和接收者极大程度的解耦,使得系统方便集成,容易扩展
模型成员:
- 消息发布者(广播发布者)
- 消息订阅者(广播接收者)
- 消息中心(AMS,Activity Manager Service)
此处我们扩展一下,观察者模式和发布订阅模式的关系
- 发布订阅模式属于广义上的观察者模式
前者时最常用的一种观察者模式的实现,且从解耦和重用角度上看更优于典型的观察者模式- 发布订阅模式加入消息中心,实现发布者和订阅者的解耦:
- 在观察者模式中,观察者需要直接订阅目标事件,在目标发出内容改变的事件后,直接接收事件并作出响应。
- 在发布订阅模式中,多了一个消息中心,一方面从发布者接收事件,另一方面向订阅者发布事件,订阅者需要从消息中心订阅事件。以此避免发布者和订阅者之间产生依赖关系。
实现流程
- 广播接收者
BroadcastReceiver
通过Binder
机制向AMS(Activity Manager Service
)进行注册; - 广播发送者通过**
binder
机制**向AMS发送广播; - AMS查找符合相应条件(
IntentFilter
/Permission
等)的BroadcastReceiver
- AMS将广播发送到上述符合条件的
BroadcastReceiver
相应的消息循环队列中 BroadcastReceiver
通过消息循环执行拿到此广播,回调BroadcastReceiver
中的onReceive()
方法。
广播发送者和广播接收者的执行是异步的,发出去的广播不会关心有无接收者接收,也不确定接收者到底是何时才能接收到。
广播的类型
主要分为一下四类:
Normal Broadcast
(普通广播):通常调用sendBroadcast(Intent)(Intent, String)
方法发送System Broadcast
(系统广播):发生各种事件时,系统自动发送Ordered Broadcast
(有序广播):调用sendOrderedBroadcast(Intent, String)
方法发送Local Broadcast
(本地广播):调用LocalBroadcastManager.sendBroadcast(intent)
方法发送Sticky Broadcast
(粘性广播):已弃用(API 21)
1. Normal Broadcast(普通广播)
开发者自定义的intent
,以Context.sendBroadcast()
,Context.sendBroadcastAsUser()
等方法发送该intent
。
- 发送示例如下:
1 | Intent intent = new Intent(); |
- 若被注册了的
BroadCastReceiver
注册的intentFilter
的action
与上述匹配,则会接收此广播,且顺序是无序的。如果发送时有相应的权限要求,则BroadCastReceiver
只有拥有相应的权限才能接受。
1 | <receiver |
2. System Broadcast(系统广播)
文末提供详细系统广播清单,不包含使用说明(位于SDK下boradcast_action.txt ),请自行查找Google官方文档
- Android系统中内置了多个系统广播,只要涉及到手机的基本操作,基本上都会发出相应的系统广播。
- 每个系统广播都具有特定的
intent-filter
,其中主要包括具体的action
,系统广播发出后,将被相应的BroadcastReceiver
接收。系统广播在系统内部当特定事件发生时,有系统自动发出。
3. Ordered Broadcast(有序广播))
- 发送出去的广播被
BroadcastReceiver
按照先后循序接收。
有序广播的有序广播中的“有序”是针对广播接收者而言的
- 发送方式:
- 定义过程与普通广播一样,调用
sendOrderedBroadcast()
,同样也有对应的sendOrderedBroadcastAsUser()
方法,只不过同样针对于预装在系统映像的应用。
- 定义过程与普通广播一样,调用
- 特点
- 按顺序接收
- 允许优先级高的
BroadcastReceiver
截断广播。 - 允许优先级高的
BroadcastReceiver
修改广播
- 接受顺序
priority
值不同:由大到小排序priority
值相同:动态注册优于静态注册
4. Local Broadcast(本地广播)
- 可以理解成一种局部广播的形式,广播的发送者和接收者都同属于一个App
- 相比于全局广播,本地广播优势体现在:
- 安全性更高;
- 更加高效。
- 引入原因:
- 其他App可能会针对性的发出与当前App
intent-filter
相匹配的广播,由此导致当前App不断接收到广播并处理; - 其他App可以注册与当前App一致的
intent-filter
用于接收广播,获取广播具体信息。
- 其他App可能会针对性的发出与当前App
- 解决方案
- 全局广播限制为局部广播(本质仍为一个全局广播)
- 使用本地广播
- 方案1的具体实现:
- 对于同一App内部发送和接收广播,将
exported
属性人为设置成false
,使得非本App内部发出的此广播不被接收; - 在广播发送和接收时,都增加上相应的
permission
,用于权限验证; - 发送广播时,指定特定广播接收器所在的包名,具体是通过
intent.setPackage(packageName)
指定在,这样此广播将只会发送到此包中的App内与之相匹配的有效广播接收器中。
- 对于同一App内部发送和接收广播,将
- 方案2的具体实现:
使用封装好的LocalBroadcastManager
类。
使用方式上与通常的全局广播几乎相同,只是注册/取消注册广播接收器和发送广播时将主调context
变成了LocalBroadcastManager
的单一实例。
对于
LocalBroadcastManager
方式发送的应用内广播,只能通过LocalBroadcastManager
动态注册的ContextReceiver
才有可能接收到(静态注册或其他方式动态注册的ContextReceiver
是接收不到的)
代码示例如下:
1 | //实例化MyBroadcastReceiver |
BroadcastReceiver
自定义BroadcastReceiver
- 继承基类
BroadcaseReceiver
- 实现抽象方法
onReceive(context, intent)
- 收到广播后,会自动回调
onReceive(..)
方法- 通常,
onReceive(..)
方法会涉及到与其他组件的交互,如发送Notification
,启动service
等- 默认情况,
BroadcaseReceiver
运行在UI线程,因此,onReceive(..)
方法不能执行耗时操作,否则ANR
- 简单的自定义Demo:
1 | //继承BroadcastReceiver基类 |
BroadcastReceiver注册类型
1. 静态注册
- 在
AndroidManifest.xml
文件中通过<receiver>
进行注册 - 规则及实例说明:
1 | <receiver |
以上述静态方法注册的MyBroadcastReceiver
,在app
首次启动时,系统或自动实例化MyBroadcastReceiver
,并注册到系统中。
2. 动态注册
- 在代码中调用
Context.registerReceiver()
, - 典型写法示例如下:
1 | public class MainActivity extends AppCompatActivity { |
注:Android中所有与观察者模式有关的设计中,一旦涉及到register,必定在相应的时机需要unregister。因此,上例在
onDestroy()
回调需要unregisterReceiver(mBroadcastReceiver)
。
广播处理机制
- 当发送串行广播(
ordered=true
)的情况下:- 静态注册的广播接收者(
receivers
),采用串行处理; - 动态注册的广播接收者(
registeredReceivers
),采用串行处理;
- 静态注册的广播接收者(
- 当发送并行广播(
ordered=false
)的情况下:- 静态注册的广播接收者(
receivers
),依然采用串行处理; - 动态注册的广播接收者(
registeredReceivers
),采用并行处理;
- 静态注册的广播接收者(
简单来说,静态注册的receivers
始终采用串行方式来处理(processNextBroadcast
); 动态注册的registeredReceivers
处理方式是串行还是并行方式, 取决于广播的发送方式(processNextBroadcast
)。
静态注册的广播往往其所在进程还没有创建,而进程创建相对比较耗费系统资源的操作,所以 让静态注册的广播串行化,能防止出现瞬间启动大量进程的喷井效应。
**ANR时机:**只有串行广播才需要考虑超时,因为接收者是串行处理的,前一个receiver
处理慢,会影响后一个receiver;并行广播 通过一个循环一次性向所有的receiver
分发广播事件,所以不存在彼此影响的问题,则没有广播超时;
- 串行广播超时情况1:某个广播总处理时间 > 2* receiver总个数 * mTimeoutPeriod, 其中
mTimeoutPeriod
,前台队列默认为10s,后台队列默认为60s; - 串行广播超时情况2:某个receiver的执行时间超过
mTimeoutPeriod
;
不同注册方式的广播接收器回调onReceive(context, intent)中的context具体类型:
- 静态注册(全局+本地):
- 回调
onReceive(context, intent)
中的context
具体指的是ReceiverRestrictedContext
- 回调
- 全局动态注册:
- 回调
onReceive(context, intent)
中的context
具体指的是Activity Context
;
- 回调
- LocalBroadcastManager动态注册
- 回调
onReceive(context, intent)
中的context
具体指的是Application Context
。
- 回调
Android 7.0以后的新特性
以上我们讨论了老生常谈的内容,下面我们谈一谈Android 7.0以后的新变化
- Android 7.0起,系统不再发送以下系统广播:
ACTION_NEW_PICTURE
ACTION_NEW_VIDEO
- 针对Android 7.0 (API级别24)和更高版本的应用程序必须通过
registerReceiver()
注册以下广播。在AndroidManifest
中声明<receiver>
起作用。CONNECTIVITY_ACTION
- Android 8.0起,应用无法在
Manifest
中注册大部分隐式系统广播(即,并非专门针对此应用的广播),此意也是在于降低随Android同时运行的应用增多,发生性能变差的几率。
出于安全考虑的广播使用最佳实践
- 如不需要向应用程序之外的组件发送广播,则可以使用支持库
Support Library
中LocalBroadcastManager
发送和接收本地广播。 - 如果许多应用程序清单中注册接收相同的广播,它会导致系统启动大量的应用程序,从而对设备性能和用户体验产生重大影响。为了避免这种情况,请使用动态注册而不是
Manifest
声明。有时,Android系统本身会强制使用上下文注册的接收器。例如,CONNECTIVITY_ACTION
广播只允许动态注册。 onReceive(Context, Intent)
运行在UI线程,不要进行耗时操作- 如耗时操作必不可少,生成子线程。
具体做法是在 onReceive() 中开启一个 service,将耗时操作置于 service 中(子线程)
- 如耗时操作必不可少,生成子线程。
- 不要使用隐含的意图传播敏感信息。这些信息可以被任何注册的应用程序读取。
- 解决方案 :
permission
/setPackage(String)
/LocalBroadcastManager
.
- 解决方案 :
- 当注册一个
BroadcastReceiver
,任何应用程序都可以发送潜在的恶意广播到你的应用的BroadcastReceiver
。- 解决方案 :
permission
/android:exported = "false"
/LocalBroadcastManager
.
- 解决方案 :
- 广播操作的命名空间是全局的。确保操作名称和其他字符串都是在您自己的名称空间中编写的,否则您可能会无意中与其他应用程序发生冲突。
- 不要从
BroadcastReceiver
开始活动,这么做会导致用户体验很差,特别是如果有不止一个BroadcastReceiver
。相反,考虑使用Notification
。 - 在Android 中如果要发送一个广播必须使用sendBroadCast 向系统发送对其感兴趣的广播接收器中。
- 使用广播必须要有一个intent 对象必设置其action动作对象
- 使用广播必须在配置文件中显式的指明该广播对象
- 每次接收广播都会重新生成一个接收广播的对象
- 在BroadCast 中尽量不要处理太多逻辑问题,建议复杂的逻辑交给Activity 或者 Service 去处理
- 如果在AndroidManifest.xml中注册,当应用程序关闭的时候,也会接收到广播。在应用程序中注册就不产生这种情况了。
参考
https://www.jianshu.com/p/e236a2669797