前言
筆者最近在探究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的官方文檔了。本文沒有深入研究廣播實現的源碼,只對應用層面以及機制上常遇到的疑問進行了梳理。對源碼的分析,會在往后對廣播的深度使用和分析后再完善。限於筆者的經驗和水平,可能很多地方表述不妥當或者難免有誤的地方,請讀者不吝賜教,謝謝。
