用廣播 BroadcastReceiver 更新 UI 介面真的好嗎?全方位解析廣播

nanchen2251發表於2019-03-03

大家好,由於公眾號有一個勘誤,所以在掘金重新更正後釋出本文。

這是 面試系列 的第三期。本期我們將來探討一下 Android 四大元件的重要組成部分:廣播 BroadcastReceiver。

往期內容傳遞:
Android 面試:說說 Android 的四種啟動模式
Android 面試:如何理解 Activity 的生命週期

前言

BroadcastReceiver 作為 Android 四大元件之一,應用場景可謂非常之多。所以我相信任何一個有一定 Android 開發經驗的工程師都不會在這個題上栽跟斗。但,某些細節,或許我們可以注意一下。

實際上我在面試過程中也遇到了這樣的題。下面請允許我用「柳學兄」的思路帶大家進入面試營。

BroadcastReceiver 內部基本原理是什麼?

Android 的廣播 BroadcastReceiver 是一個全域性的監聽器,主要用於監聽 / 接收應用發出的廣播訊息,並作出響應。其採用了設計模式中的 觀察者模式 ,可將廣播基於 訊息訂閱者訊息釋出者訊息中心(AMS:即 Activity Manager Service)解耦,通過 Binder 機制形成訂閱關係。

圖片來源於網路
圖片來源於網路

說說 BroadcastReceiver 的兩種註冊方式

Android 廣播的兩種註冊方式肯定難不倒任何人,實際上我估計也只有對少量的 Android 開發面試者才會遇到這樣的題,這裡不會有什麼特別的,熟悉的可以直接跳過

  • 靜態註冊
    靜態註冊廣播的方式只需要在 AndroidManifest.xml 裡通過 標籤宣告。下面附上一些屬性說明。

    <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 這個十字路口猶豫不決的時候,還會糾結選擇嗎?

歡迎關注南塵的公眾號:nanchen
如果你喜歡,你可以選擇分享給大家。如果你有好的文章,歡迎投稿,讚賞全部歸你所有。

                      長按上方二維碼關注
                  做不完的開源,寫不完的矯情
                  一起來看 nanchen 的成長筆記複製程式碼

相關文章