Android 面試題:EventBus 發送的消息,如何作到線程切換?

一. 序

EventBus 是一個基於觀察者模式的事件訂閱/發佈框架,利用 EventBus 能夠在不一樣模塊之間,實現低耦合的消息通訊。java

EventBus 由於其使用簡單且穩定,被普遍應用在一些生產項目中。面試

一般咱們就是使用 EventBus 分發一些消息給消息的訂閱者,除此以外咱們還能夠經過 EventBus 將消息傳遞到不一樣的線程中去執行,處理消息。這其中還涉及到一些線程切換問題、線程池的問題,在使用的過程當中,還有一些配置的選擇,此時咱們須要根據不一樣的業務場景,來選擇不一樣的線程切換方式。安全

本文就 EventBus 的幾種線程切換方式,以及內部的實現原來,來分析如何使用 EventBus 來切換消息線程。框架

二. EventBus 的線程切換

2.1 EventBus 切換線程

EventBus 是一個基於觀察者模式的事件訂閱/發佈框架。利用 EventBus 能夠在不一樣模塊之間,實現低耦合的消息通訊。async

EventBus 誕生以來這麼多年,在不少生產項目中均可以看到它的身影。而從更新日誌能夠看到,除了體積小,它還很穩定,這兩年就沒更新過,最後一次更新也只是由於支持全部的 JVM,讓其使用範圍不只僅侷限在 Android 上。ide

可謂是很是的穩定,穩定到讓人有一種感受,要是你使用 EventBus 出現了什麼問題,那必定是你使用的方式不對。oop

EventBus 的使用方式,對於 Android 老司機來講,必然是不陌生的,相關資料太多,這裏就再也不贅述了。post

在 Android 下,線程的切換是一個很經常使用並且很必須的操做,EventBus 除了能夠訂閱和發送消息以外,它還能夠指定接受消息處理消息的線程。學習

也就是說,不管你 post() 消息時處在什麼線程中,EventBus 均可以將消息分發到你指定的線程上去,聽上去就感受很是的方便。優化

不過不管怎麼切換,無外乎幾種狀況:

  • UI 線程切子線程。
  • 子線程切 UI 線程。
  • 子線程切其餘子線程。

在咱們使用 EventBus 註冊消息的時候,能夠經過 @Subscribe 註解來完成註冊事件, @Subscribe 中能夠經過參數 threadMode 來指定使用那個線程來接收消息。

@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventTest(event:TestEvent){
  // 處理事件
}
複製代碼

threadMode 是一個 enum,有多種模式可供選擇:

  1. POSTING,默認值,那個線程發就是那個線程收。
  2. MAIN,切換至主線程接收事件。
  3. MAIN_ORDERED,v3.1.1 中新增的屬性,也是切換至主線程接收事件,可是和 MAIN 有些許區別,後面詳細講。
  4. BACKGROUND,確保在子線程中接收事件。細節就是,若是是主線程發送的消息,會切換到子線程接收,而若是事件自己就是由子線程發出,會直接使用發送事件消息的線程處理消息。
  5. ASYNC,確保在子線程中接收事件,可是和 BACKGROUND 的區別在於,它不會區分發送線程是不是子線程,而是每次都在不一樣的線程中接收事件。

EventBus 的線程切換,主要涉及的方法就是 EventBus 的 postToSubscription() 方法。

private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
  switch (subscription.subscriberMethod.threadMode) {
    case POSTING:
      invokeSubscriber(subscription, event);
      break;
    case MAIN:
      if (isMainThread) {
        invokeSubscriber(subscription, event);
      } else {
        mainThreadPoster.enqueue(subscription, event);
      }
      break;
    case MAIN_ORDERED:
      if (mainThreadPoster != null) {
        mainThreadPoster.enqueue(subscription, event);
      } else {
        // temporary: technically not correct as poster not decoupled from subscriber
        invokeSubscriber(subscription, event);
      }
      break;
    case BACKGROUND:
      if (isMainThread) {
        backgroundPoster.enqueue(subscription, event);
      } else {
        invokeSubscriber(subscription, event);
      }
      break;
    case ASYNC:
      asyncPoster.enqueue(subscription, event);
      break;
    default:
      throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
  }
}
複製代碼

能夠看到,在 postToSubscription() 方法中,對咱們配置的 threadMode 值進行了處理。

這端代碼邏輯很是的簡單,接下來咱們看看它們執行的細節。

2.2 切換至主線程接收事件

想在主線程接收消息,須要配置 threadMode 爲 MAIN。

case MAIN:
  if (isMainThread) {
    invokeSubscriber(subscription, event);
  } else {
    mainThreadPoster.enqueue(subscription, event);
  }
複製代碼

這一段的邏輯很清晰,判斷是主線程就直接處理事件,若是是非主線程,就是用 mainThreadPoster 處理事件。

追蹤 mainThreadPoster 的代碼,具體的邏輯代碼都在 HandlerPoster 類中,它實現了 Poster 接口,這就是一個普通的 Handler,只是它的 Looper 使用的是主線程的 「Main Looper」,能夠將消息分發到主線程中。

爲了提升效率,EventBus 在這裏還作了一些小優化,值得咱們借鑑學習。

爲了不頻繁的向主線程 sendMessage(),EventBus 的作法是在一個消息裏儘量多的處理更多的消息事件,因此使用了 while 循環,持續從消息隊列 queue 中獲取消息。

同時爲了不長期佔有主線程,間隔 10ms (maxMillisInsideHandleMessage = 10ms)會從新發送 sendMessage(),用於讓出主線程的執行權,避免形成 UI 卡頓和 ANR。

MAIN 能夠確保事件的接收,在主線程中,須要注意的是,若是事件就是在主線程中發送的,則使用 MAIN 會直接執行。爲了讓開發和可配置的成都更高,在 EventBus v3.1.1 新增了 MAIN_ORDERED,它不會區分當前線程,而是統統使用 mainThreadPoster 來處理,也就是必然會走一遍 Handler 的消息分發。

當事件須要在主線程中處理的時候,要求不能執行耗時操做,這沒什麼好說的,另外對於 MAIN 或者 MAIN_ORDERED 的選擇,就看具體的業務要求了。

2.3 切換至子線程執行

想要讓消息在子線程中處理,能夠配置 threadMode 爲 BACKGROUND 或者 AYSNC,他們均可以實現,可是也有一些區別。

先來看看 BACKGROUND,經過 postToSubscription() 中的邏輯能夠看到,BACKGROUND 會區分當前發生事件的線程,是不是主線程,非主線程這直接分發事件,若是是主線程,則 backgroundPoster 來分發事件。

case BACKGROUND:
	if (isMainThread) {
		backgroundPoster.enqueue(subscription, event);
	} else {
		invokeSubscriber(subscription, event);
	}
break;
複製代碼

BackgroundPoster 也實現了 Poster 接口,其中也維護了一個用鏈表實現的消息隊列 PendingPostQueue,

在一些編碼規範裏就提到,不要直接建立線程,而是須要使用線程池。EventBus 也遵循這個規範,在 BackgroundPoster 中,就使用了 EventBus 的 executorService 線程池對象去執行。

爲了提升效率,EventBus 在處理 BackgroundPoster 時,也有一些小技巧值得咱們學習。

能夠看到,在 BackgroundPoster 中,處理主線程拋出的事件時,同一時刻只會存在一個線程,去循環從隊列中,獲取事件處理事件。

經過 synchronized 同步鎖來保證隊列數據的線程安全,同時利用 volatile 標識的 executorRunning 來保證不一樣線程下看到的執行狀態是可見的。

既然 BACKGROUND 在處理任務的時候,只會使用一個線程,可是 EventBus 卻用到了線程池,看似有點浪費。可是再繼續瞭解 ASYNC 的實現,才知道怎麼樣是對線程池的充分利用。

和前面介紹的 threadMode 同樣,大多數都對應了一個 Poster,而 ASYNC 對應的 Poster 是 AsyncPoster,其中並無作任何特殊的處理,全部的事件,都是無腦的拋給 EventBus 的 executorService 這個線程池去處理,這也就保證了,不管如何發生事件的線程,和接收事件的線程,必然是不一樣的,也保證了必定會在子線程中處理事件。

public void enqueue(Subscription subscription, Object event) {
	PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
	queue.enqueue(pendingPost);
	eventBus.getExecutorService().execute(this);
}
複製代碼

到這裏應該就理解了 BACKGROUNDASYNC ,雖然均可以保證在子線程中接收處理事件,可是內部實現是不一樣的。

BACKGROUND 同一時間,只會利用一個子線程,來循環從事件隊列中獲取事件並進行處理,也就是前面的事件的執行效率,會影響後續事件的執行。例如你分發了一個事件,使用的是 BACKGROUND 可是隊列前面還有一個耗時操做,那你分發的這個事件,也必須等待隊列前面的事件都處理完成才能夠繼續執行。因此若是你追求執行的效率,馬上立刻就要執行的事件,可使用 ASYNC

那是否是都用 ASYNC 就行了?固然這種一攬子的決定都不會好,具體問題具體分析,ASYNC 也有它本身的問題。

ASYNC 會無腦的向線程池 executorService 發送任務,而這個線程池,若是你不配置的話,默認狀況下使用的是 Executors 的 newCachedThreadPool() 建立的。

這裏我又要說到編碼規範了,不推薦使用 Executors 直接建立線程,之因此這樣,其中一個緣由在於線程池對任務的拒絕策略newCachedThreadPool 則會建立一個無界隊列,來存放線程池暫時沒法處理的任務,說到無界隊列,拍腦殼就能想到,當任務(事件)過多時,會出現的 OOM。

這也確實是 EventBus 在使用 ASYNC 時,真實存在的問題。

可是其實這裏讓開發者本身去配置,也很難配置一個合理的線程池的拒絕策略,拒絕時必然會放棄一些任務,也就是會放棄掉一些事件,任何放棄策略都是不合適的,這在 EventBus 的使用中,表現出來就是出現邏輯錯誤,該收到的事件,收不到了。因此你看,這裏無界隊列不合適,可是不用它呢也不合適,惟一的辦法就是合理的使用 ASYNC,只在必要且合理的狀況下,纔去使用它。

三. 小結時刻

到這裏基本上 EventBus 在分發事件時的線程切換,就講清除了,不少資料裏其實都寫了他們能夠切換線程,可是對於一些使用的細節,描述的並不清除,正好藉此文,把 EventBus 的線程切換的直接講清除。

EventBus 也是簡歷上比較常見的高頻詞,我在面試的過程當中,也常常會問面試者,關於它是如何作到線程切換的問題。可是正由於它簡單易用,其實不少時候咱們都忽略了它的實現細節。

今天就到這裏,小結一下:

1. EventBus 能夠經過 threadMode 來配置接收事件的線程。

2. MAIN 和 MAIN_ORDERED 都會在主線程接收事件,區別在因而否區分,發生事件的線程是不是主線程。

3. BACKGROUND 確保在子線程中接收線程,它會經過線程池,使用一個線程循環處理全部的事件。因此事件的執行時機,會受到事件隊列前面的事件處理效率的影響。

4. ASYNC 確保在子線程中接收事件,區別於 BACKGROUND,ASYNC 會每次向線程池中發送任務,經過線程池的調度去執行。可是由於線程池採用的是無界隊列,會致使 ASYNC 待處理的事件太多時,會致使 OOM。

本文就到這裏,本文對你有幫助嗎?留言、轉發、收藏是最大的支持,謝謝!


本文首發自公衆號「承香墨影」,歡迎關注獲取最新的原創文章。公衆號後臺回覆成長『成長』,將會獲得我準備的學習資料,。

相關文章
相關標籤/搜索