EventBus 消息的執行緒切換模型與實現原理
- 2019 年 10 月 3 日
- 筆記
一. 序
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,有多種模式可供選擇:
- POSTING,默認值,那個執行緒發就是那個執行緒收。
- MAIN,切換至主執行緒接收事件。
- MAIN_ORDERED,v3.1.1 中新增的屬性,也是切換至主執行緒接收事件,但是和 MAIN 有些許區別,後面詳細講。
- BACKGROUND,確保在子執行緒中接收事件。細節就是,如果是主執行緒發送的消息,會切換到子執行緒接收,而如果事件本身就是由子執行緒發出,會直接使用發送事件消息的執行緒處理消息。
- 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); }
到這裡應該就理解了 BACKGROUND 和 ASYNC ,雖然都可以保證在子執行緒中接收處理事件,但是內部實現是不同的。
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。
本文就到這裡,本文對你有幫助嗎?留言、轉發、收藏是最大的支援,謝謝!
本文首發自公眾號「承香墨影」,歡迎關注獲取最新的原創文章。公眾號後台回復成長『成長』,將會得到我準備的學習資料,。

