“入職接手舊項目,所有網絡請求數據通過 EventBus 分發,嚇得我想離職...”

題圖 by Jianqiu Jia

一. 序

雖然現在互聯網行業的就業形式「相當嚴峻」,張小胖還是成功跳槽漲薪。

入職第一天 Leader 說,“你剛來,這周先熟悉熟悉咱們的項目吧”。

張小胖熟練的用 Git pull 代碼到本地,環境變量一通配置,終於把項目跑了起來,看着項目裏的網絡請求數據,居然全是靠 EventBus 分發,陷入了深深的沉思…

在子線程請求數據,再通過 EventBus 將數據分發到主線程,這是什麼騷操作?這難道不會有問題嗎?


雖然 EventBus 可以做到多模塊之間低耦合的事件通信,可完全利用 EventBus 去做線程切換,解耦是解耦了,但靠譜的項目根本不會這麼幹。

不過既然聊到了 EventBus 的線程切換,那今天就深入聊聊當 EventBus 事件分發,遇上線程切換的時候,是如何處理的。以及使用的時候有什麼需要注意的,大量的依賴 EventBus 的線程切換,會不會有效率問題。

二. EventBus 的線程切換

2.1 EventBus 切換線程

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

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

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

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

在 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。

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


聯機圓桌」????推薦我的知識星球,一年 50 個優質問題,上桌聯機學習。

公衆號後臺回覆成長『成長』,將會得到我準備的學習資料。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章