這是 面試系列 的第三期。本期我們將來探討一下 Android 四大組件的重要組成部分:廣播 BroadcastReceiver。
往期內(nèi)容傳遞:
Android 面試:說說 Android 的四種啟動模式
Android 面試:如何理解 Activity 的生命周期
前言
BroadcastReceiver 作為 Android 四大組件之一,應(yīng)用場景可謂非常之多。所以我相信任何一個有一定 Android 開發(fā)經(jīng)驗的工程師都不會在這個題上栽跟斗。但,某些細(xì)節(jié),或許我們可以注意一下。
實際上我在面試過程中也遇到了這樣的題。下面請允許我用「柳學(xué)兄」的思路帶大家進(jìn)入面試營。
BroadcastReceiver 內(nèi)部基本原理是什么?
Android 的廣播 BroadcastReceiver 是一個全局的監(jiān)聽器,主要用于監(jiān)聽 / 接收應(yīng)用發(fā)出的廣播消息,并作出響應(yīng)。其采用了設(shè)計模式中的 觀察者模式 ,可將廣播基于 消息訂閱者 、消息發(fā)布者、消息中心(AMS:即 Activity Manager Service)解耦,通過 Binder 機(jī)制形成訂閱關(guān)系。

說說 BroadcastReceiver 的兩種注冊方式
Android 廣播的兩種注冊方式肯定難不倒任何人,實際上我估計也只有對少量的 Android 開發(fā)面試者才會遇到這樣的題,這里不會有什么特別的,熟悉的可以直接跳過。
- 靜態(tài)注冊
靜態(tài)注冊廣播的方式只需要在 AndroidManifest.xml 里通過 <receiver> 標(biāo)簽聲明。下面附上一些屬性說明。
<receiver
android:enabled=["true" | "false"]
//此 broadcastReceiver 能否接收其他 App 發(fā)出的廣播
//默認(rèn)值是由 receiver 中有無 intent-filter 決定的:如果有 intent-filter,默認(rèn)值為 true,否則為 false
android:exported=["true" | "false"]
android:icon="drawable resource"
android:label="string resource"
//繼承 BroadcastReceiver 子類的類名
android:name=".mBroadcastReceiver"
//具有相應(yīng)權(quán)限的廣播發(fā)送者發(fā)送的廣播才能被此 BroadcastReceiver 所接收;
android:permission="string"
// BroadcastReceiver 運行所處的進(jìn)程
// 默認(rèn)為 App 的進(jìn)程,可以指定獨立的進(jìn)程
//注:Android 四大基本組件都可以通過此屬性指定自己的獨立進(jìn)程
android:process="string" >
//用于指定此廣播接收器將接收的廣播類型
//本示例中給出的是用于接收網(wǎng)絡(luò)狀態(tài)改變時發(fā)出的廣播
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
- 動態(tài)注冊
動態(tài)注冊方式是通過調(diào)用Context下面的registerReceiver()進(jìn)行注冊,可以調(diào)用unregisterReceiver()進(jìn)行注銷。需要注意的是:動態(tài)廣播最好在 Activity 的onResume()注冊,并在onPause()進(jìn)行注銷。
為什么建議動態(tài)廣播盡量在 onPause() 進(jìn)行注銷?
我們可以先看看 Activity 的生命周期。

首先有注冊就得有注銷,否則一定會造成內(nèi)存泄漏。注意上面途中紅框圈住的部分。,閱讀官方源碼發(fā)現(xiàn),當(dāng)系統(tǒng)因為內(nèi)存不足需要回收 Activity 占用的資源時,Activity 在執(zhí)行完 onPause() 方法后就可能面臨著被銷毀的危險,有些生命周期方法,如:onStop()、onDestroy() 根本就不會執(zhí)行,而 onPause() 由于一定會調(diào)用的特殊性,自然是避免內(nèi)存泄漏的好方法。
兩種注冊方式的區(qū)別也是可以用圖一目了然。

說說 Android 的常用廣播類型吧
基本在 Android 領(lǐng)域常用的方式就是直接調(diào)用 Context 提供的方法 sendBroadcast() 和 sendOrderBroadcase() 發(fā)送無序廣播和有序廣播。
無序廣播
無序廣播是完全異步的,通過Context.sendBroadcast()方法來發(fā)送,從效率上來看,還算是比較高的。正如它的名稱一樣,無序廣播對所有的廣播接收者而言,是無序的。也就是說,所有接收者無法確定接收時序的順序,這樣也導(dǎo)致了,無序廣播無法被停止。當(dāng)它被發(fā)送出去之后,它將通知所有這條廣播的接收者,直到?jīng)]有與之匹配的廣播接收者為止。有序廣播
有序廣播通過Context.sendOrderedBroadcast()方法來發(fā)送。有序廣播和無序廣播最大的不同,就是它可以允許接收者設(shè)定優(yōu)先級,它會按照接收者設(shè)定的優(yōu)先級依次傳播。而高優(yōu)先級的接收者,可以對廣播的數(shù)據(jù)進(jìn)行處理或者停止掉此條廣播的繼續(xù)傳播。廣播會先發(fā)送給優(yōu)先級高 (android:priority) 的 Receiver,而且這個 Receiver 有權(quán)決定是繼續(xù)發(fā)送到下一個 Receiver 或者是直接終止廣播。
除了無序廣播和有序廣播,還有其他的類型嗎?
可能還是有不少的朋友知道 Sticky 廣播方式。
- 粘性廣播 Sticky
Sticky 廣播和它的名字很像,它是一個具有粘性的廣播。它被發(fā)出去之后,會一直滯留在系統(tǒng)中,直到有與之匹配的接收者,才會將其發(fā)出去。它采用Context.sendStickyBroadcast()方法進(jìn)行發(fā)送廣播。
從官方文檔上可以看到,如果想要發(fā)送一個 Sticky 廣播,需要具有BROADCAST_STICKY權(quán)限,這個可以在 AndroidManifest.xml 中進(jìn)行注冊,而如果沒有此權(quán)限,則會拋出SecurityException異常。
對于系統(tǒng)而言,只會保留最后一條 Sticky 廣播,并且會一直保留下去,也就是說,如果我們發(fā)送的 Sticky 廣播不被取消,當(dāng)有一個接收者的時候就會收到它,再來一個還是能收到。所有我們需要在合適的實際,調(diào)用 removeStickyBoradcast() 方法,將其取消掉。
從官方文檔中也可以看到 StickyBroadcast 已經(jīng)被標(biāo)記為 @Deprecated ,出于一些安全的考慮,已經(jīng)將其標(biāo)記為廢棄,不再推薦使用。我們作為開發(fā)者,對于一些被標(biāo)記為 @Depracated 的方法,使用起來還是需要謹(jǐn)慎的。
有時候基于數(shù)據(jù)安全考慮,我們想發(fā)送廣播只有自己(本進(jìn)程)能接收到,怎么處理?
首先,Android 中的廣播可以跨進(jìn)程通信,因為 exported 對于有 Intent-filter 的情況下默認(rèn)為 true。所以我們難以有這樣的需求:
- 對于某些敏感性的廣播,我們不希望暴露給外部。
- 其他 App 可能會發(fā)出和當(dāng)前 App intent-filter 相匹配的廣播,導(dǎo)致 App 不斷進(jìn)行廣播接收和處理。
這真是一個壞消息,我們必須讓我們的應(yīng)用變得有效率并足夠的安全。
一般我們能自然地想到在注冊廣播的時候把 exported 值設(shè)為 false 并給 App 的廣播增加上權(quán)限,可問題是權(quán)限不夠是一個字符串,面對當(dāng)前如此強(qiáng)大的反編譯技術(shù),這終究是不安全的。
為了解決這樣的問題,我們不難想到可以通過往主線程的消息池(Message Queue)里發(fā)送消息,讓其做到只有主線程的 Handler 可以分發(fā)處理它。或者在發(fā)送廣播的時候直接通過 Intent.setPackage(packageName) 指定廣播接收器的包名。
要不是我們項目中有個 BroadcastUtil 工具類,我還之前真不知道 Support V4 包下還有這么一個 LocalBroadcastManager 本地廣播類。
本地廣播 在 Android Support v4 : 21 版本后加入了我們的大家庭。它使用 LocalBroadcastManager (以下簡稱 LBM)類來管理。
LocalBroadcast 的使用非常的簡單,只需要將 Broadcast 的對應(yīng) API,替換為 LBM 為我們提供的 API 即可。
LBM 是一個單例對象,可以使用 LocalBroadcastManager.getInstance(Context context) 方法獲取到。在 Context 中定義的和 Broadcast 相關(guān)的方法,在 LBM 中都有對應(yīng)的 API 。非常有意思的是,LBM 為了區(qū)分異步和同步,使用了 sendBroadcast() 和 sendBroadcastSync() 方法來做為區(qū)分。
在 Android 中用廣播來更新 UI 界面好嗎?
廢話扯了這么多,終于說到標(biāo)題上的問題了。
直接回答:可以,為什么不可以呢?在實際開發(fā)中我們不是經(jīng)常這么用么?
很好,可以肯定你是一個真實的 Android 開發(fā)者了,不過在認(rèn)證你的「合格」之前,想問問 BroadcastReceiver 的生命周期。
什么?BroadcastReceiver 的生命周期?糟糕,面試前只復(fù)習(xí)了 Activity 和 Fragment 的生命周期,雜還有人問 BroadcastReceiver 的生命周期。
所以,你支支吾吾了。
其實還是有比較多的人了解 BroadcastReceiver 的生命周期的。BroadcastReceiver 有生命周期,但比較短,而且很短。當(dāng)它的 onReceive() 方法執(zhí)行完成后,它的生命周期也就隨之結(jié)束了。這時候由于 BroadcastReceiver 已經(jīng)不處于 active 狀態(tài),所以極有可能被系統(tǒng)干掉。也就是說如果你在 onReceive() 去開線程進(jìn)行異步操作或者打開 Dialog 都有可能在沒達(dá)到你要的結(jié)果時進(jìn)程就被系統(tǒng)殺掉了。
所以,正確答案是?
更新 UI 界面這個定義太廣泛了。實際開發(fā)中其實大多數(shù)情況都是可以采用 BroadcastReceiver 來更新 UI,所以也造成了很多人回答就想上面很肯定和自信的回答可以。
實際上我們知道 Receiver 也是運行在主線程的,不能做耗時操作。雖然超時時間相對于 Activity 的 5 秒更高,有足足的 10 秒。但不意味著我們實際開發(fā)中所有的更新 UI 界面操作時間都在安全范圍之內(nèi)。
此外,對于頻繁更新 UI,也不推薦這種方式。Android 廣播的發(fā)送和接收都包含了一定的代價,它的傳輸都是通過 Binder 進(jìn)程間通信機(jī)制來實現(xiàn)的,那么系統(tǒng)肯定會為了廣播能順利傳遞而做一些進(jìn)程間通信的準(zhǔn)備。而且可能會由于其它因素導(dǎo)致廣播發(fā)送和到達(dá)不準(zhǔn)時(或者說接收會延遲)。
這種情況可能嗎?
很可能,而且很容易發(fā)生。我們要先了解 Android 的 ActivityManagerService 有一個專門的消息隊列來接收發(fā)送出來的廣播,sendBroadcast() 執(zhí)行完后就立即返回,但這時發(fā)送來的廣播只是被放入到隊列,并不一定馬上被處理。當(dāng)處理到當(dāng)前廣播時,又會把這個廣播分發(fā)給注冊的廣播接收分發(fā)器ReceiverDispatcher,ReceiverDispatcher 最后又把廣播交給接 Receiver 所在的線程的消息隊列去處理(就是你熟悉的 UI 線程的 Message Queue)。
整個過程從發(fā)送 ActivityManagerService 到 ReceiverDispatcher 進(jìn)行了兩次 Binder 進(jìn)程間通信,最后還要交到 UI 的消息隊列,如果基中有一個消息的處理阻塞了 UI,當(dāng)然也會延遲你的 onReceive() 的執(zhí)行。
BroadcastReceiver 和 EventBus 有啥不同?
EventBus 作為 GitHub 上一個頗受歡迎的庫,目前也是有著 16.3 k 的星星,足以見其強(qiáng)大。
所以在不少面試中當(dāng)然會遇到這樣的提問。這不,筆者在咕咚面試的時候就被面試官問到了這個題,又一個打臉,當(dāng)時我像被電了一番,答的并不怎么樣。
眾所周知,廣播是 Android 的四大組件之一。系統(tǒng)系統(tǒng)級的事件都是通過廣播來通知的,比如說網(wǎng)絡(luò)的變化、電量的變化、短信接收和發(fā)送狀態(tài)等。所以,如果是和 Android 系統(tǒng)相關(guān)的通知,我們還得選擇本地廣播。
但是?。?!廣播相對于其他實現(xiàn)方式,是很重量級的,它消耗的資源較多。它的優(yōu)勢體現(xiàn)在和 SDK 的緊密聯(lián)系,onReceive() 方法自帶了 Context 和 Intent 參數(shù),所以在一定意義上實現(xiàn)了便捷性,但如果對 Context 和 Intent 應(yīng)用很少或者說只做很少的交互的話,使用廣播真的就是一種浪費?。?!
那 EventBus 呢?
先說說其優(yōu)點:
調(diào)度靈活
要說到優(yōu)點,這一定是我最先想到的。因為它真的是太靈活了,在實際開發(fā)中感覺它就是一個機(jī)靈鬼,想去哪就去哪,根本就不需要像廣播一樣關(guān)注 Context 的注入與傳遞。父類對于通知的監(jiān)聽和處理還可以直接繼承給子類,可以設(shè)置優(yōu)先級讓 Subscriber 關(guān)注到優(yōu)先級更高的通知,其粘滯事件(sticky events)能夠保證通知不會因 Subscriber 的不在場而忽略。可繼承、優(yōu)先級、粘滯,是 EventBus 比之于廣播、觀察者等方式最大的優(yōu)點,它們使得創(chuàng)建結(jié)構(gòu)良好組織緊密的通知系統(tǒng)成為可能。使用簡單
進(jìn)入到 EventBus 的官網(wǎng),看一眼 README.md,簡直不能再簡單,簡簡單單三個步驟,再在 build.gradle 中添加一個依賴,輕輕松松搞定有木有?如果不想創(chuàng)建 EventBus 的實例,還可以直接調(diào)用靜態(tài)方法EventBus.getDefault()獲取。快速且輕量
作為一個 GitHub 的明星項目,性能方面是可以放心的。
EventBus 這么棒,那我們有組建通信就用 EventBus 吧。
還真是人無完人,物無完物。EventBus 也有著它的致命弱點。EventBus 最大的缺點在于其邏輯性,直接看其代碼,一不小心根本看不通有沒有?另外一個問題是,當(dāng)程序較大后,觀察者獨有的接口膨脹缺點也會伴隨著你的項目,你能想象很多 Event 后綴類的感覺嗎?
綜上,EventBus 由于其針對統(tǒng)一進(jìn)程,所以在某些復(fù)雜的情況下單純依靠接口回調(diào)不好處理組件通信的時候,直接去嘗試 EventBus 吧。
說了這么多,在廣播和 EventBus 這個十字路口猶豫不決的時候,還會糾結(jié)選擇嗎?