這是 面試系列 的第三期。本期我們將來探討一下 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 這個十字路口猶豫不決的時候,還會糾結選擇嗎?