Android 面試(三):用廣播 BroadcastReceiver 更新 UI 界面真的好嗎?

這是 面試系列 的第三期。本期我們將來探討一下 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)系。

圖片來源于網(wǎng)絡(luò)

說說 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 的生命周期。

圖片來源于網(wǎng)絡(luò)

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

兩種注冊方式的區(qū)別也是可以用圖一目了然。


圖片來源于網(wǎng)絡(luò)

說說 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é)選擇嗎?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容