【朝花夕拾】四大组件之(一)Broadcast篇
前言
笔者最近在探究ANR及源码的过程中,发现对Broadcast的一些应用层面上的知识有的感觉比较生疏,有的记忆不准确,有的认识不完整。所谓“基础不牢,地动山摇”,于是就梳理了一下Broadcast的一些知识点,查漏补缺,加深对它的全面认识。该篇文章是基于源码、官网、工作经验以及实验结果完成的,阅读本文需要一定的基础,如果是初学者,理解起来可能有一定的难度,需要一定的耐心。
本文主要包含如下内容:
一、整体认识
广播是一个全局的监听器,可以监听者整个系统,也可以监听者整个app。一般我们说“广播是Android的四大组件之一”,但准确点说应该是“广播接收者(Broadcast Receiver)是Android的四大组件之一”,它也是四大组件中最简单的一个。但“麻雀虽小,五脏俱全”,广播有着自己的生命周期,有着丰富的类型,对性能有着巨大的影响,能用于跨进程通信和进程内组件间通信,是系统ANR发送的根源之一......
Android开发者官网中对广播的介绍以及开发者帮助文档如下:【Broadcasts overview】【BroadcastReceiver开发帮助文档】。
二、基本原理
广播的实现使用了设计模式中的观察者模式,基于消息的发布/订阅事件模型,这其中有3个角色:(1)消息发布者(广播发送者);(2)消息中心(AMS:Activity Manager Service);(3)消息订阅者(广播接收者)。这使得广播的发送者和接收者高度解耦,使用非常方便。以下序列图(不是严格意义上的序列图,勿喷)显示了广播实现的基本流程及原理,其中第一步,第二步,第四步需要开发者手动来完成,其他的由系统自动完成。广播的发送和接收是需要消息,广播发送后,不确定一定有接收者,也不确定接收者什么时候会接收到。
图2.1 广播实现及原理流程图
三、广播的注册
广播的注册方式有静态注册和动态注册之分,静态注册是指在AndroidManifest.xml中进行注册,动态注册是指在代码中进行注册。
1、静态注册
从Android8.0开始,系统对静态注册增加了很大的限制,很多以往版本能够正常使用的静态注册的广播,从该版本开始很可能就会失效。这一点在后文中“不同Android系统版本中广播机制的重要变迁”这一节中会详细介绍,这里不赘述。
(1)静态注册中的属性简介
Android开发者官网【receiver属性】【intent-filter属性】中对这些属性做了详细的说明,咱们这里做一些翻译及补充。
1 <receiver 2 android:directBootAware=["true" | "false"] 3 android:enabled=["true" | "false"] 4 android:exported=["true" | "false"] 5 android:icon="drawable resource" 6 android:label="string resource" 7 android:name=".MBroadcastReceiver" 8 android:permission="string" 9 android:process="string" > 10 <intent-filter android:icon="drawable resource" 11 android:label="string resource" 12 android:priority="integer"> 13 <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> 14 </intent-filter> 15 </receiver>
- android:directBootAware
是否可以直接启动属性。这个属性应该是Android 7.0(Android N)开始才添加上去的,因为从Android7.0开始采用文件加密系统FBE。该属性不仅仅在<receiver>标签中有,在其它组件和<application>中也可以设置,只是影响的范围不一样。在<application>中该属性为true的情况下:如果<receiver>中这个属性值为false,那么手机重启后,在屏幕没有解锁的情况下,该广播所能访问到的数据都是加密的,将不能启动;如果为true,则可以被启动。其默认值为false,也就是说手机重启后在没有解锁的情况下,该广播不能被启动。
对于文件加密系统FBE,比较重要,内容也有点复杂,这里推荐两份官网的资料:【直接启动】、【文件级加密】。
- android:enabled
定义系统是否能够实例化这个广播接收器。如果设置为true,表示能够被实例化;如果为false,表示不能被实例化;默认值为true。<application>中也有该属性,用于设置设置所有组件是否能实例化,所以只有当<application>和<receiver>中这个属性值都为true,该广播接收器才能够被启动,只要有一个为false,那么都会被禁止实例化。
- android:exported
这个属性用于指示该广播接收器是否可以接收来其他App或不同userID App发送的广播。如果设置为true,表示可以接收;如果设置为false,表示只能接收同一个app或有相同userID的App发出的广播。它的默认值比较特殊,依赖于是否有intent-filter属性,如果有则默认值为true,否则为false。如果为false,该接收器只能由指定了明确类名的Intent对象来调用,这就意味着该接收器只能在应用程序内部使用,因为通常在应用程序外部并不知道这个类名,一般来说某个广播对外都是以action的方式提供入口的。另一方面,intent-filter属性的作用就是用于接收外部App或系统广播的,自然而然默认值就是true了。
除了这个属性外,还有后面的“android:permission”属性,也可以用于限制哪些intent实体可以向该接收器发送广播。
- android:icon
该属性定义代表该广播接收器的图标,其值为图片资源。<application>中也有icon属性,而且是所有组件中该属性的默认值。也就是说,包括广播接收者在内的所有组件在没有自己独立设置该属性的情况下,都用<application>中icon属性的值,如果组件自己设置了,就以自己设置的为准。
后面每个<intent-filter>中也有icon属性,会以这里<receiver>中icon的属性值作为默认值。
- android:label
该属性给该接收器设定了一个用户可读的文本标签,其值为string类型。它的默认值特性以及对后面<intent-filter>的影响都和icon类似,这里就不赘述了。
- android:name
该属性指定了广播接收器,是BroadcastReceiver的子类,其值为自定义的广播接收器类的全名,如“com.example.songwei.MBroadcastReceiver”,或者为了便捷可以省略掉包名,如“.MBroadcastReceiver”(假设在<manifest>中定义的包名为"com.example.songwei”)。这个类就是真正处理广播事件的地方,没有默认值,必须要指定,且不能随便填写,否则编译不过。如果类名或路径有修改,该属性值一定要同步,如果没有填写错误的话,在开发工具中,点击该值可以直接跳转到该类。
- android:permission
该属性用于指定该接收器能接受到指定广播所必须拥有的权限,该权限在广播发送者处定义。如果此处没有设置,就以<application>中的permission值为默认值。如果<application>中也没有设置,就认为该接收器不受权限保护。当然,如果两处都设置了该值,以此处为准。
- android:process
该属性指定了该接收器运行的进程名。一般来说,所有组件都运行在app默认创建的进程中,该进程名与包名相同。如果这里没有设值,就以<application>中该属性值为默认值;如果<application>中也没定义,就运行在App的默认进程中;如果两处都设值了,就以此处为准。
如果该值以“:”打头,当收到广播后,就会创建一个当前App私有的进程,这个进程会以该值为进程名,且广播接收器会运行在这个进程中。如果该值以小写字母打头,接收器就会运行在以该值为进程名的全局进程中,这样可以让不同app中的不同组件可以共享一个进程,从而降低资源的损耗。
- intent-filter
这个元素主要用于根据action值来过滤满足条件的广播。一个<receiver>中可以定义多个<intent-filter>,每个<intent-filter>中又有icon,lable等属性来表征自己。
- android:priority
该属性用于设置优先级。有序广播OrderedBroadcast的接收者们将按照该值的大小依次接收,如果大小相同则谁先注册谁先接收。取值范围为-1000~10000,数值越大优先级越高。
- action
该属性用于匹配是否为需要接收的广播。
(2)静态注册与AMS
2、动态注册
(1)动态注册与AMS
registerReceiver的方法有如下4个(来源于AndroidStudio的提示),这里选取第一个作为例子来分析一下:
我们知道,无论是Activity还是Service,都是Context的子类,其继承链如下所示:
ContextWrapper.java中重写了registerReceiver方法,所以按住Ctrl键并在AndroidStudio中点击该方法,会跳转到如下代码中:
ContextWrapper.java(extends Context.java)
1 Context mBase; 2 public ContextWrapper(Context base) { 3 mBase = base; 4 } 5 ...... 6 @Override 7 public Intent registerReceiver( 8 BroadcastReceiver receiver, IntentFilter filter) { 9 return mBase.registerReceiver(receiver, filter); 10 }
Context.java是一个抽象类,registerReceiver方法也是在这个抽象类中定义的抽象方法。
Context.java
1 @Nullable 2 public abstract Intent registerReceiver(@Nullable BroadcastReceiver receiver,IntentFilter filter);
我们知道面向接口编程,是在接口或抽象类中定义,在具体实现类或子类中具体实现。该抽象方法,实际是在ContextImpl中实现的:
ContextImpl.java(extends Context.java)
1 @Override 2 public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { 3 return registerReceiver(receiver, filter, null, null); 4 } 5 ...... 6 @Override 7 public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, 8 String broadcastPermission, Handler scheduler) { 9 return registerReceiverInternal(receiver, getUserId(), 10 filter, broadcastPermission, scheduler, getOuterContext(), 0); 11 } 12 ...... 13 private Intent registerReceiverInternal(BroadcastReceiver receiver, int userId, 14 IntentFilter filter, String broadcastPermission, 15 Handler scheduler, Context context, int flags) { 16 ...... 17 18 final Intent intent = ActivityManager.getService().registerReceiver( 19 mMainThread.getApplicationThread(), mBasePackageName, rd, filter, 20 broadcastPermission, userId, flags); 21 ...... 22 }
ActivityManager中getService()实际上是一个Binder:
ActivityManager.java
1 public static IActivityManager getService() { 2 return IActivityManagerSingleton.get(); 3 } 4 5 private static final Singleton<IActivityManager> IActivityManagerSingleton = 6 new Singleton<IActivityManager>() { 7 @Override 8 protected IActivityManager create() { 9 final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE); 10 final IActivityManager am = IActivityManager.Stub.asInterface(b); 11 return am; 12 } 13 };
通过Binder方式,实际上就调用AMS中的registerReceiver方法了:
ActivityManagerService.java
1 public Intent registerReceiver(IApplicationThread caller, String callerPackage, 2 IIntentReceiver receiver, IntentFilter filter, String permission, int userId, 3 int flags) { 4 ...... 5 }
一步步跟踪过来可以看到,我们应用层的广播注册代码registerReceiver最终转移到了AMS中,这就对应上了“图2.1 广播实现及原理流程图”中的第2步了。其它的几个注册广播函数也最终调用了AMS中的registerReceiver方法,只是传进来的参数值不一样而已。
(2)取消注册
如果是动态注册,需要在相应的生命周期中取消注册(有的地方也称为反注册)。取消注册的函数只有如有一个:
我们按照上文注册广播的方式追踪代码,可以发现最后也是到了AMS中来实现的。
ActivityManagerService.java
1 public void unregisterReceiver(IIntentReceiver receiver) { 2 ...... 3 }
四、广播的发送
发送广播的函数比较多,如下图所示,我们可以看到Sticky(粘性)广播已经被设置为过时方法了。这里选取最简单的sendBroadcast(Intent intent)来分析。
我们参照前面动态广播注册的方法追踪源码,关键流程如下
Context.java
1 public abstract void sendBroadcast(@RequiresPermission Intent intent);
ContextWrapper.java
1 ContextWrapper.java(extends Context.java) 2 Context mBase; 3 public ContextWrapper(Context base) { 4 mBase = base; 5 } 6 ...... 7 @Override 8 public Intent registerReceiver( 9 BroadcastReceiver receiver, IntentFilter filter) { 10 return mBase.registerReceiver(receiver, filter); 11 }
ContextImpl.java
1 ContextImpl.java(extents Context.java) 2 @Override 3 public void sendBroadcast(Intent intent) { 4 warnIfCallingFromSystemProcess(); 5 String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); 6 try { 7 intent.prepareToLeaveProcess(this); 8 ActivityManager.getService().broadcastIntent( 9 mMainThread.getApplicationThread(), intent, resolvedType, null, 10 Activity.RESULT_OK, null, null, null, AppOpsManager.OP_NONE, null, false, false, 11 getUserId()); 12 } catch (RemoteException e) { 13 throw e.rethrowFromSystemServer(); 14 } 15 }
ActivityManagerService.java
1 public final int broadcastIntent(IApplicationThread caller, 2 Intent intent, String resolvedType, IIntentReceiver resultTo, 3 int resultCode, String resultData, Bundle resultExtras, 4 String[] requiredPermissions, int appOp, Bundle bOptions, 5 boolean serialized, boolean sticky, int userId) { 6 enforceNotIsolatedCaller("broadcastIntent"); 7 synchronized(this) { 8 intent = verifyBroadcastLocked(intent); 9 10 final ProcessRecord callerApp = getRecordForAppLocked(caller); 11 final int callingPid = Binder.getCallingPid(); 12 final int callingUid = Binder.getCallingUid(); 13 final long origId = Binder.clearCallingIdentity(); 14 int res = broadcastIntentLocked(callerApp, 15 callerApp != null ? callerApp.info.packageName : null, 16 intent, resolvedType, resultTo, resultCode, resultData, resultExtras, 17 requiredPermissions, appOp, bOptions, serialized, sticky, 18 callingPid, callingUid, userId); 19 Binder.restoreCallingIdentity(origId); 20 return res; 21 } 22 }
这里就对应上了“图2.1 广播实现及原理流程图”中的第5步了。其他的几个发送广播的方法,也都最终调用到了如上的broadcastIntentLocke()方法中,也只是其中的参数值不同而已。所以真正发送广播的逻辑实现,是在AMS中来完成的。
五、广播的类型
根据不同的角度,广播可以分为不同的类型,这些分类基本都是根据广播发送者来决定的。下面咱们来详细探讨一下这些类型。
1、自定义广播和系统广播
根据发送的广播是用户自己定义的还是由系统定义的,可将广播分为自定义广播和系统广播。这个比较简单,容易理解,不过多说明,这里可以了解一下Android系统为我们定义了哪些广播【Android系统广播大全】。
2、普通广播和有序广播
根据广播发送者发送的是否为有序广播,可以将广播分为普通广播(无序广播)和有序广播。
(1)普通广播
普通广播,有的文章中称为标准广播,以异步的方式发送广播给接收者。Android系统AMS(ActivityManagerService)发出广播后,所有满足条件的广播几乎可以同时受到广播,没有顺序之分,也无需向AMS返回处理结果。这种方式下,各个接收者之间不会相互干扰,效率较高。大致形式如下图所示:
(2)有序广播
有序广播是以一种同步的方式向接收者发送广播的。广播接收者会根据开发者设定的优先级进行排序,AMS会先给最高优先级接收者发送广播,该接收器处理完广播后需要返回处理结果给AMS,然后再向次优先级接收者发送广播,依此类推。广播接收者可以把处理结果传递给是下一个接收者,也可以终止广播的传递。如果某个接收者超时或者终止了广播,那么后面的接收者也将无法再收到广播,所以效率会比较低。短信拦截功能,就是根据这个原理来实现的。有序广播传递的大致情形如下图所示:
有序广播的使用可以参考一下【Android中广播接收者BroadcastReceiver详解】,其中对比普通广播,有几个关键的地方需要注意:1)android:priority用于设置接收器优先级。2)getResultData()获取广播数据。3)setResultData()向下一接收者传递数据。4)abortBroadcast()用于终止广播的传递。
3、全局广播和局部广播
根据发送的广播希望被接收的范围,可以将广播分为全局广播和局部广播。从机制上看,如果发送广播的可以被其他app(以app进程为限)接收,那么该广播为全局广播;如果只能被app内部接收,那么该广播为局部广播。
(1)全局广播
我们平时使用的Broadcast就是全局广播,因为这些广播可以在整个系统中进行传播。全局广播在安全和性能方面存在一些问题,无疑增加了系统的负担,比如发送的广播携带的一些数据信息可以被其他app接收到;app外部发送的一些垃圾广播也会被app意外接收并产生一些响应;多个满足匹配要求的广播接收器可能同时收到一个广播等。为了解决这方面的问题,可以通过设置“android:exported”,permission,setPackage等各种方式来限定接收范围,从而在安全和性能方面进行优化。
(2)局部广播简介
Android也提供了另外一种更为简单且效率更高的方式——局部广播。局部广播,也叫做本地广播,Android v4 兼容包提供android.support.v4.content.LocalBroadcastManager工具类用于实现局部广播。谷歌官方文档的介绍如下【LocalBroadcastManager官方文档】:
Helper to register for and send broadcasts of Intents to local objects within your process. This has a number of advantages over sending global broadcasts with sendBroadcast(Intent):
● You know that the data you are broadcasting won't leave your app, so don't need to worry about leaking private data.
● It is not possible for other applications to send these broadcasts to your app, so you don't need to worry about having security holes they can exploit.
● It is more efficient than sending a global broadcast through the system.
这里献丑大致翻译一下:
(LocalBroadcastManager)用于在你的(当前app)进程中帮助你注册和发送Intent的广播I到本地对象。相比于通过sendBroadcast(Intent)的方式发送全局广播,这种方式有不少优势:
● 你所传播的数据不会离开当前你的app,所以无需担心会泄漏私人数据。 ● 其他app无法发送广播到你的app,所以你无需担心这些广播导致的安全漏洞。 ● 比起通过系统来实现发送的全局广播,这种方式更高效(不需要发送给整个系统)。
(3)局部广播的使用
局部广播的使用也比较简单,基本使用如下:
1 /** 2 * 1、自定义广播action 3 */ 4 public static final String MY_ACTION = "com.songwei.action.MY_ACTION"; 5 6 /** 7 * 2、发送局部广播 8 */ 9 private void sendBroadcast() { 10 LocalBroadcastManager.getInstance(mContext).sendBroadcast( 11 new Intent(MY_ACTION) 12 ); 13 } 14 15 /** 16 * 3、自定义广播接收器 17 */ 18 private class MyBroadcastReceiver extends BroadcastReceiver { 19 20 @Override 21 public void onReceive(Context context, Intent intent) { 22 //处理具体的逻辑 23 } 24 } 25 26 /** 27 * 4、注册/取消注册广播 28 */ 29 private MyBroadcastReceiver mReceiver = new MyBroadcastReceiver(); 30 31 private void registerLoginBroadcast() { 32 IntentFilter intentFilter = new IntentFilter(MY_ACTION); 33 LocalBroadcastManager.getInstance(mContext).registerReceiver(mReceiver, intentFilter); 34 } 35 36 private void unRegisterLoginBroadcast() { 37 LocalBroadcastManager.getInstance(mContext).unregisterReceiver(mReceiver); 38 }
从上面的代码可以发现,与全局广播的使用相比,仅仅就是多了粗斜体部分的代码,就是在sendBroadcast/registerReceiver/unRegisterReceiver这几个方法前都加上了LocalBroadcastManager.getInstance(mContext)。
(4)局部广播注意事项
相比于全局广播,有几点需要特别注意:
1)该方式只能通过代码动态注册,不能在AndroidManifest.xml文件中静态注册。
2)一定不要忘记前面提到的三个方法前加上LocalBroadcastManager.getInstance(mContext),否则可能接收不到广播或者无法实现局部广播的效果。
3)其他方面如在对应的地方要取消注册,onReceive()方法中不进行耗时操作等,和全局广播一致,这里不赘述。
4、前台广播和后台广播
在阅读关于ANR的文章的时候,我们经常会看到类似这样的描述:对于BroadcastReceiver事件中onReceive()方法,在规定时间内没有执行完成会导致ANR,前台广播的规定时间是10s,后台广播是60s。根据发送的广播被接收的优先级,可以将广播分为前台广播和后台广播。
(1)前台广播
添加Intent.FLAG_RECEIVER_FOREGROUND这个flag,可以将广播设置为前台广播。我们知道,广播发出后,广播接收器会有不少的延迟后才会收到广播,这样对于一些紧急的事件肯定是不利的。为了减少这个延时,可以将该广播设置为前台广播,当发送该前台广播时,会允许接收者以前台的优先级运行,优先接受并处理该广播事件。当然这也就要求广播接收器在更短的时间内(10s) 完成广播事件,我们知道,在没有特别处理的情况下,onReceive方法是运行在UI线程的,如果不尽快处理完,前台广播这个拥有特权的广播就会对整个系统产生较大的干扰。前台广播的设置代码如下所示:
1 Intent mIntent = new Intent(string action); 2 mIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 3 mContext.sendBroadcast(mIntent);
(2)后台广播
如果不添加Intent.FLAG_RECEIVER_FOREGROUND这个flag,系统默认该广播为后台广播,或者将flag设置为Intent.FLAG_FROM_BACKGROUND,也可以将广播设置为后台广播,此时对应的广播接收者的优先级为后台优先级。
这里我们看出,这里的前台广播和后台广播之分,是针对广播的接收优先级而言的,而不是取决于是否与用户有交互行为,更不是onReceiver方法处于UI线程还是后台工作线程。对于这两个flag的含义可以看一下参考资料【Android Intent的FLAG详解】。
5、并发广播和串行广播
根据接收和处理广播事件的方式是并行的还是串行的,可以把广播分为并发广播和串行广播。
(1)并行广播
并行广播在有的文章中也被称为平行广播、并行广播等,它是指在接收并处理广播时,所有接收者是无序的,它们之间相互独立,没有相互依赖的关系。在很多程序员的认知中,普通广播都是并行广播,因为它们的广播接收器不像有序广播那样,需要设置优先级。事实上这种认知是错误的,在AndroidManifest.xml中静态注册的普通广播其实是串行广播,而只有在代码中动态注册的普通广播才是真正的并行广播。
(2)串行广播
串行广播是指广播接收者会根据优先级或注册时间等因素进行排序,来一个接着一个地处理广播事件。前面提到的有序广播,就是串行广播,静态注册的普通广播也是串行广播。
(3)总结
为了加深印象和纠正错误的认识,可以对这两类广播简单做一点归纳和总结:
(4)参考资料
对于并发广播和串行广播的分类,如下资料从源码的角度对此进行了细致的分析,有兴趣的读者可以深入探索:
【[Android]BroadcastQueue如何分发广播(四)】
【说说Android的广播(3) - 什么样的广播是并发的?】
6、粘性广播和非粘性广播
根据发送广播的方法中是否有sticky字样,可以分为粘性广播和非粘性广播。粘性广播即Sticky Broadcast,在Android5.0及以后,粘性广播已经deprecated,和它相关的粘性有序广播,也同样deprecated,这里就不深入了。
六、广播接收器生命周期
作为Android的四大组件之一,广播接收器自然也有自己的生命周期。只是它的生命周期非常简单且非常短,只有onReceive一个回调方法,当收到广播产生onReceive回调开始,到onReceive方法执行完并return,广播接收器的生命周期便宣告结束。
七、onReceive方法所在上下文
根据广播不同的作用范围和注册方式,广播接收器的onReceive(Context context,Intent intent)方法所持有的上下文context存在一些差异。
1、局部广播(LocalBroadcastManager方式),只有动态动态注册方式,context的返回值为:Application Context实例。演示结果如下(注:这里没有自定义Application,用的系统默认的):
02-14 11:36:03.903 8698-8698/com.example.demos I/BroadcastDemo: context=android.app.Application@d6e8e9d
2、全局广播静态注册,这种情况下context的返回值为:ReceiverRestrictedContext实例。演示结果如下(注:这是在Android6.0设备上测试的结果,Android8.0开始对隐性静态注册做了很大的限制):
02-14 11:29:51.175 8159-8159/com.example.demos I/BroadcastDemo: context=android.app.ReceiverRestrictedContext@b8e7e15
3、全局广播动态注册,这种情况下和当前所在组件有关,如果是Activity中,那么context的返回值为:当前Activity Context实例;如果是Service,那么context的返回值为:当前Service Context实例。演示结果如下(注:当前注册的广播所在组件为分别为BroadcastDemoActivity和MyService):
1 02-14 11:09:35.692 6322-6322/com.example.demos I/BroadcastDemo: context=com.example.demos.BroadcastDemoActivity@3fee6c4 2 ... 3 02-14 11:52:04.427 9854-9854/com.example.demos I/BroadcastDemo: context=com.example.demos.MyService@59ce1f7
八、onReceive方法所在线程
有时候会碰到有人问到这样的问题“广播的onReceive方法一定运行在UI线程吗?”,看过一些非权威的博客文章,有的说是一般情况是在UI线程,但有些情况例外;有的说是早期版本官网里面说的是一般情况下是UI线程,后来版本中说法改为一定是在UI线程中。稗官野史不足为信,所以笔者在当前最新的官网上查找了很长时间,这方面资料很少,但在【Broadcast overview—Security considerations and best practices】文章倒数第二段中有如下说明:
Because a receiver's onReceive(Context, Intent) method runs on the main thread, it should execute and return quickly.
既然官网上都这样说了,那么咱们也就按照这里说的来,认定“广播的onReceive方法一定运行在UI线程”吧。
九、onReceive方法处理耗时操作
我们知道,一般情况,广播接收机的onReceive方法是执行在UI线程的,这就决定了在该方法中不允许直接处理耗时操作,否则会报ANR。一般如果在Activity和Service中处理耗时操作,可以通过new Thread开启子线程来完成,但是在广播接收器中不可以这样做。
1、原因
因为如果在onReceive方法中开启了子线程后,onReceive方法执行完后,该广播接收器生命周期便结束了,系统便认为该组件消亡了。如果当前进程中没有其他组件处于活动状态,那么整个app进程就成为了一个空进程,当系统内存紧张时,空进程就是首先会被系统杀死并收回内存的,这样一来,在onReceive方法中开启的子线程就无法完成耗时的任务了。而Activity和Service生命周期都比较长,一般情况下会有充足的时间完成耗时操作,不太容易被系统杀死。官方文档【Processes and Application Lifecycle:https://developer.android.google.cn/guide/components/activities/process-lifecycle】 第四段对此有明确的说明:
A common example of a process life-cycle bug is a BroadcastReceiver that starts a thread when it receives an Intent in its BroadcastReceiver.onReceive() method ......
官方文档【Broadcasts overview—Effects on process state:https://developer.android.google.cn/guide/components/broadcasts#effects-process-state】也做了详细的讲解:
For this reason, you should not start long running background threads from a broadcast receiver ......
2、解决办法
为了解决该问题,上述两个官方网站中明确给出了两种方法:JobService和goAsync()。另外还有被广大程序员们使用的startService方法,下面简单介绍一下这些方法。
(1)JobService
这种方法在上面两个官网中都提到了,可惜笔者没有具体研究过这种方法,这里就不多说了,有兴趣的读者可以自行研究。
(2)goAsync()
这种方法是在文档【Broadcasts overview—Effects on process state】中提到的,且明确给出了示例,实例很简单,是在AsyncTask中结合PendingResult来实现的,如下所示:
1 public class MyBroadcastReceiver extends BroadcastReceiver { 2 private static final String TAG = "MyBroadcastReceiver"; 3 4 @Override 5 public void onReceive(Context context, Intent intent) { 6 final PendingResult pendingResult = goAsync(); 7 Task asyncTask = new Task(pendingResult, intent); 8 asyncTask.execute(); 9 } 10 11 private static class Task extends AsyncTask { 12 13 private final PendingResult pendingResult; 14 private final Intent intent; 15 16 private Task(PendingResult pendingResult, Intent intent) { 17 this.pendingResult = pendingResult; 18 this.intent = intent; 19 } 20 21 @Override 22 protected String doInBackground(String... strings) { 23 StringBuilder sb = new StringBuilder(); 24 sb.append("Action: " + intent.getAction() + "\n"); 25 sb.append("URI: " + intent.toUri(Intent.URI_INTENT_SCHEME).toString() + "\n"); 26 String log = sb.toString(); 27 Log.d(TAG, log); 28 return log; 29 } 30 31 @Override 32 protected void onPostExecute(String s) { 33 super.onPostExecute(s); 34 // Must call finish() so the BroadcastReceiver can be recycled. 35 pendingResult.finish(); 36 } 37 } 38 }
(3)startService
这个方法见得比较多,在onReceive中启动Servcie,Service生命周期长,能在后台运行,在其中开启子线程来完成耗时操作。不要使用bindService,因为我们知道,通过bind方式启动Service,Service的生命周期会和调用者广播接收器绑定,当广播接收器消亡后,Service也会被销毁,仍然起不到效果。
通过前面对不能使用子线程的原因说明,我们可以得知,主要是避免app进程被系统收回导致子线程无法完成。如果是在系统app或系统进程等这样常驻内存的情况下,就不用担心进程被轻易杀死了,那么此时在onReceive方法中使用子进程,应该是没有问题的。
十、不同Android系统版本中广播机制的重要变迁
随着Android版本的升级,系统对app的性能、安全、用户体验等很多方法都做有提升。自然而然,广播机制也在一步一步地完善之中,如下就对不同版本的重要变迁做一些盘点。
1、Android3.1中广播机制的变迁
在3.1版本之前,app静态注册广播后,在收到对应广播时,及时该app已经退出,也能收到广播。从3.1开始,情况有所变化:系统在广播Intent的flag增加了两个参数FLAG_INCLUDE_STOPPED_PACKAGES和FLAG_EXCLUDE_STOPPED_PACKAGES,命名可以看出,前者表示包含已经停止的包(即已经退出的app),后者表示不包含已经停止的包。如果广播的flag被设置为后者,那么即使是静态注册了广播,只要该app进程已经退出了,就无法再接收到广播。这一点我们很容易理解,因为这样做可以对系统的安全和性能大有裨益。
从3.1开始,系统对广播的flag默认设置为了FLAG_EXCLUDE_STOPPED_PACKAGES,对于系统广播而言,由系统内部发出,开发者无法修改intent中flag的值,所以如果App进程已经退出了,将无法再收到系统广播。而对于自定义广播而言,可以通过如下的方式覆盖掉系统默认的方式。
1 Intent intent = new Intent(); 2 ... 3 intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); 4 sendBroadcast(intent);
这一点可以查看Android官方文档【Android3.1版本变更—广播机制变更:https://developer.android.google.cn/about/versions/android-3.1#launchcontrols】的相关章节说明。
2、Android5.0中广播机制的变更
Android5.0中的广播机制的变更主要是在粘滞广播上,从该版本开始,普通粘滞广播和有序粘滞广播都被置为过期功能,以后不再建议使用。
3、Android7.0中广播机制的变更
从该版本开始,系统移除了两项隐式广播:ACTION_NEW_PICTURE和ACTION_NEW_VIDEO(分别用于监听拍照和视频),也就是说系统将不会再发送这两个隐式广播了。还有一项隐式广播CONNECTIVITY_ACTION(用于监听网络变化),从该版本开始,只能通过动态注册接收,静态注册将失效。
更详细的说明,可以参考Android官方文档【Android7.0版本变更—广播机制变更:https://developer.android.google.cn/about/versions/nougat/android-7.0-changes#bg-opt】。
4、Android8.0中广播机制变更
Android8.0主要对隐式广播的静态注册做增加了限制,这个变更的影响比较大。以下从Android开发者官方文档和实验验证入手,对该版本中广播的变更进行探究。
(1)“Broadcaset overview”中的描述
Android开发者官网对广播概述【广播概述https://developer.android.google.cn/guide/components/broadcasts】中,有如下说明:
Beginning with Android 8.0 (API level 26), the system imposes additional restrictions on manifest-declared receivers. If your app targets Android 8.0 or higher, you cannot use the manifest to declare a receiver for most implicit broadcasts (broadcasts that don't target your app specifically). You can still use a context-registered receiver when the user is actively using your app.
这里献丑翻译一下:
从Android8.0开始(API级别26),系统对manifest中静态注册广播加强了额外的限制条件。 如果你的应用targets在Android8.0或以上,你将不能在manifest中为大部分的隐式广播(那些目标不是专门针对你的应用的广播)进行声明。但是当用户主动使用你的app时,你仍然可以使用context-registered(即动态注册)的方式注册。
从上述文字可以得到如下信息:
1)文中说的是“manifest中不能对大部分隐式广播进行声明”,不是说所有隐式广播都不能注册。事实上,有很多隐式系统广播仍然可以使用静态注册,官方文档【隐式广播静态注册豁免清单https://developer.android.google.cn/guide/components/broadcast-exceptions】
中列出了不受此限制的系统广播,但同时也特别做了如下说明,以建议开发者避免使用静态注册:
Note: Even though these implicit broadcasts still work in the background, you should avoid registering listeners for them.
2)该限制针对的是manifest中注册的隐式广播,显示广播不在此列。所谓隐式广播,就是以intent-filter中的“action”属性来匹配的广播;而显式广播,是通过广播接收器包名和类名来直接匹配的广播。
显示广播的注册代码,“name”中不需要必须是完整的广播接收器类路径。
1 <receiver 2 android:name=".MyReceiver" 3 ...... 4 </receiver>
显示广播的发送代码,ComponentName的两个参数分别是包名和广播接收器完整路径。
1 Intent intent = new Intent(); 2 intent.setComponent(new ComponentName("com.example.demos","com.example.demos.MyReceiver")); 3 sendBroadcast(intent);
3)动态注册广播不受该限制。
4)从该版本开始,所有自定义的隐式广播,静态注册后都将无效。这一点,笔者测试时,跨app发送了一段自定义隐式广播,在Android6.0设备上可以收到,而在Android8.0设备上却无法收到。
(2)“后台执行限制”文档中的说明
在官方文档【Android后台执行限制https://developer.android.google.cn/about/versions/oreo/background#broadcasts】中也做了更详细的描述,内容比较多且已经翻译为了中文,这里就不整段摘抄了,读者可以进入该链接细读。以下仅提取部分信息:
● 应用可以继续在它们的清单中注册显式广播。
● 应用可以在运行时使用 Context.registerReceiver() 为任意广播(不管是隐式还是显式)注册接收器。
● 需要签名权限的广播不受此限制所限。
● 在许多情况下,之前注册隐式广播的应用使用 JobScheduler 作业可以获得类似的功能。
(3)小结
以上对Android8.0中广播的变更描述比较多,为了方便记忆,这里总结一下关键点:
1)静态注册的隐式广播,除了“豁免清单”中声明的系统广播外,其他的将不再生效,包括自定义的广播。
2)静态注册的显示广播、动态注册的所有广播、需要签名权限的广播,均不受影响。
3)为了兼容,以前静态注册的隐式广播可以使用 JobScheduler 替代。
4)最重要的一点:正如官网中所说,尽量避免使用静态注册。咱们开发者在后续开发中,直接使用动态注册吧。
5、Android9.0中广播机制变更
该版本中主要对网络广播NETWORK_STATE_CHANGED_ACTION和WIFI相关的广播所携带的隐私数据做了限制,一些重要的隐私数据将无法通过广播来获取,需要通过API中的相关函数来得到。 详细可以参考如下文章:
【广播概述https://developer.android.google.cn/guide/components/broadcasts】
结语
本文参考了不少官方文档的内容,事实上最好的帮助文档和学习资料就是这套google的官方文档了。本文没有深入研究广播实现的源码,只对应用层面以及机制上常遇到的疑问进行了梳理。对源码的分析,会在往后对广播的深度使用和分析后再完善。限于笔者的经验和水平,可能很多地方表述不妥当或者难免有误的地方,请读者不吝赐教,谢谢。