EventBus源碼分析(三)
在前面的兩篇文章中分別對EventBus中的構建,分發事件以及注冊方法的查詢做了分析,本篇文章是最后一部分,主要針對EventBus對于注冊方法的調用作學習了解,前面兩篇文章的鏈接如下:
EventBus源碼分析(一)
EventBus源碼分析(二)
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 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);
}
}
這個方法的代碼也可以清晰地說明了在不同的threadMode情況下,注冊方法在不同線程調用的情況。
在理解注冊方法在哪個線程被調用之前需要首先明確postToSubscription()方法在哪個線程被調用,該方法是由EventBus對象的post()方法經過一系列方法調用的,所以它運行在用戶發送事件的線程中,參數isMainThread表示該運行線程是否為主線程,下面四種threadMode中,注冊方法被調用的線程分別為:
- POSTING:最簡單的一種模式,即直接在事件發送的線程中調用注冊方法;
- MAIN:在主線程中調用注冊方法,如果事件發送線程不是主線程,則由mainThreadPoster將方法調用派送到主線程中調用,其實就是使用的Handler機制,后面再做分析;
- BACKGROUND:在后臺線程中調用,如果事件發送不是在主線程,則注冊方法則直接在該線程中被調用,如果在主線程發送事件,則注冊方法由backgroundPoster派發到工作線程中調用;
- ASYNC:直接由asyncPoster派發到工作線程中調用,至于它與BACKGROUND的區別,后面會根據源碼做分析。
所以從該方法中可以看出,在切換不同線程調用注冊方法,是通過三個Poster完成的,下面分別對它們的源碼做分析。
1. HandlerPoster
首先mainThreadPoster的類型為HandlerPoster,從名字可以看出它應該與Handler有關。在android的編程中,如果一些耗時操作的結果需要更新UI,那么需要通過定義Handler的形式,將結果加入到主線程的消息隊列,然后由自定義在主線程的Handler對消息做處理。那么這里的原理也是一致的。首先看HandlerPoster的代碼:
final class HandlerPoster extends Handler {
private final PendingPostQueue queue;
private final int maxMillisInsideHandleMessage;
private final EventBus eventBus;
private boolean handlerActive;
HandlerPoster(EventBus eventBus, Looper looper, int maxMillisInsideHandleMessage) {
super(looper);
this.eventBus = eventBus;
this.maxMillisInsideHandleMessage = maxMillisInsideHandleMessage;
queue = new PendingPostQueue();
}
...
}
可以看到HandlerPoster是繼承自Handler,那么它則是根據構造器中傳入的Looper對象,在handleMessage()方法中處理對應線程中消息隊列的消息,那么我們再看在EventBus對象初始化時,mainThreadPoster初始化是否為主線程的Looper, EventBus的構造器中的部分代碼為:
mainThreadPoster = new HandlerPoster(this, Looper.getMainLooper(), 10);
Looper.getMainLooper()說明了mainThreadPoster的handleMessage方法是在主線程中被調用,就可以實現將注冊方法派發到主線程中調用了。其他參數后面部分一一分析。
首先來看PendingPostQueue這個明顯是一個隊列,其代碼有興趣的可以自行查看,它是自定義的隊列,沒有實現Java定義的Queue接口,這里只是簡單定義了內部的隊列數據結構以及入隊和出隊方法,只不過有一個與時間相關的出隊方法,后面用到時會提到。PendingPostQueue內部的隊列結構中的元素即為PendingPost,它封裝了一次注冊方法調用所需要的注冊信息和事件類型,即Subscription類型對象和事件對象。類的代碼很簡單,不再貼出,但是PendingPost中有一個對象的緩存池,最多可以保存10000個PendingPost對象,避免了對象的創建和回收。有興趣的可以查看源碼。
下面看HandlerPoster的入隊方法:
void enqueue(Subscription subscription, Object event) {
PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
synchronized (this) {
queue.enqueue(pendingPost);
if (!handlerActive) {
handlerActive = true;
if (!sendMessage(obtainMessage())) {
throw new EventBusException("Could not send handler message");
}
}
}
}
首先根據注冊信息和事件對象構建一個PendingPost加入隊列,然后通過發送消息通知Handler自身調用handleMessage()方法,這里的handlerActive是一個標志位,標志當前是否正在執行處理PendingPostQueue隊列中的PendingPost,也就是正在調用隊列中的注冊方法。下面為handleMessage()方法:
@Override
public void handleMessage(Message msg) {
boolean rescheduled = false;
try {
long started = SystemClock.uptimeMillis();
while (true) {
PendingPost pendingPost = queue.poll();
if (pendingPost == null) {
synchronized (this) {
// Check again, this time in synchronized
pendingPost = queue.poll();
if (pendingPost == null) {
handlerActive = false;
return;
}
}
}
eventBus.invokeSubscriber(pendingPost);
long timeInMethod = SystemClock.uptimeMillis() - started;
if (timeInMethod >= maxMillisInsideHandleMessage) {
if (!sendMessage(obtainMessage())) {
throw new EventBusException("Could not send handler message");
}
rescheduled = true;
return;
}
}
} finally {
handlerActive = rescheduled;
}
}
這里代碼的邏輯就是循環遍歷隊列,通過eventBus.invokeSubscriber(pendingPost)方法執行注冊方法,invokeSubscriber()方法在第一篇文章中已經分析過。兩次判斷pendingPost是否為空是處理多線程的同步問題,這里與單例模式是同樣的道理。不過這里需要注意的是EventBus為HandlerPoster處理調用注冊方法設定了時間上限,也就是構造器中初始化的maxMillisInsideHandleMessage,在EventBus中初始化mainThreadPost時為其指定了10ms, 也就是while循環超過10ms之后會退出handleMessage方法,并將handleActive置位true,并再次發送消息,使得該handler再次調用handleMessage()方法,繼續處理隊列中的注冊信息,這是為了避免隊列過長是,while循環阻塞主線程造成卡頓。
以上就是HandlerPoster的全部代碼,十分簡短,通過Handler的方式將注冊方法的調用派發到其他線程,在EventBus中只是將其用于在主線程中調用注冊方法的情況。
2. BackgroundPoster
下面為BackgroundPoster的源碼:
final class BackgroundPoster implements Runnable {
private final PendingPostQueue queue;
private final EventBus eventBus;
private volatile boolean executorRunning;
BackgroundPoster(EventBus eventBus) {
this.eventBus = eventBus;
queue = new PendingPostQueue();
}
...
}
首先BackgroundPoster是繼承自Runnable,是一個可執行的任務,這里看到BackgroundPoster中也有隊列和標志位,其執行邏輯與HandlerPoster應該極為相似,下面看一下它的入隊方法:
public void enqueue(Subscription subscription, Object event) {
PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
synchronized (this) {
queue.enqueue(pendingPost);
if (!executorRunning) {
executorRunning = true;
eventBus.getExecutorService().execute(this);
}
}
}
代碼邏輯很簡單,構建PendingPost,并在同步塊中將該對象入隊,然后調用線程池中的線程執行該Runnable,即在其他線程中執行run方法,進而調用注冊方法,下面看run方法代碼:
@Override
public void run() {
try {
try {
while (true) {
PendingPost pendingPost = queue.poll(1000);
if (pendingPost == null) {
synchronized (this) {
// Check again, this time in synchronized
pendingPost = queue.poll();
if (pendingPost == null) {
executorRunning = false;
return;
}
}
}
eventBus.invokeSubscriber(pendingPost);
}
} catch (InterruptedException e) {
Log.w("Event", Thread.currentThread().getName() + " was interruppted", e);
}
} finally {
executorRunning = false;
}
}
這里的while循環,除了沒有設定時間上限之外,幾乎與HandlerPoster中handleMessage方法中的處理邏輯完全一致,只不過標志位改成了executorRunning,其作用也幾乎是一致的。但是這里需要注意有另外一個與時間有關的方法,即queue.poll(1000),這里有一個等待過程,即從隊列中取PendingPost對象時,如果沒有PendingPost會等待1000毫秒,其代碼如下:
synchronized PendingPost poll(int maxMillisToWait) throws InterruptedException {
if (head == null) {
wait(maxMillisToWait);
}
return poll();
}
這里是典型的生產者和消費者模型,并且設定了時間上限。因此也就說明了BackgroundPoster會將1000毫秒以內入隊的注冊信息在同一個線程中調用,這一點也是與AsyncPoster最重要的區別。BackgroundPoster的代碼也介紹完了,同樣十分簡單,下面繼續分析AsyncPoster中的代碼,下面你會看到,其實現更簡單。
3. AsyncPoster
由于AsyncPoster的代碼實現比較簡單,這里一次性全部貼出它的代碼:
class AsyncPoster implements Runnable {
private final PendingPostQueue queue;
private final EventBus eventBus;
AsyncPoster(EventBus eventBus) {
this.eventBus = eventBus;
queue = new PendingPostQueue();
}
public void enqueue(Subscription subscription, Object event) {
PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
queue.enqueue(pendingPost);
eventBus.getExecutorService().execute(this);
}
@Override
public void run() {
PendingPost pendingPost = queue.poll();
if(pendingPost == null) {
throw new IllegalStateException("No pending post available");
}
eventBus.invokeSubscriber(pendingPost);
}
}
AsyncPoster同樣是繼承自Runnable,但是可以看到這里沒有了標志位和while循環,入隊的邏輯就是構建PendingPost,然后加入隊列,這里注意enqueue是同步的方法,不用擔心同步問題,然后調用線程池的線程執行該Runnable,run方法的邏輯就是取出隊列的pendingPost, 并執行invokeSubscriber方法,由于是入隊一次調用一次execute方法,所以正常情況下隊列中不會取不出pendingPost, 也不會剩余pendingPost。
這里AsyncPoster更為簡單,但是重點是需要思考一下它與BackgroundPoster的區別,前面也提到過BackgroundPoster設定一個時間上限,并且在該段時間內入隊的注冊信息會在同一個線程中調用,而AsyncPoster則是完全異步的調用。也就是說BackgroundPoster會盡可能在同一線程中調用注冊方法,但不保證,可是它保證有一定的順序,也就是注冊信息收集的順序,而AsyncPoster對注冊方法的調用則是完全異步,不確定在哪個線程,也不確定順序。
至此,對于注冊方法調用的線程調度分析源碼已經分析完了,總結起來只是三個不同的Poster, HandlerPoster是繼承自Handler, 通過handler機制可以將線程調度分派給主線程,而BackgroundPoster和AsyncPoster則是繼承自Runnable,覆寫run方法定義調用注冊方法的任務,并有線程池負責調度啟動該任務,進而完成對注冊方法的調用。
通過三篇文章,對EventBus的源碼大致分析了一遍,下圖對分析的流程大致做一個總結,EventBus主要負責不同對象間傳遞消息,并且可以很方便地切換線程調用注冊方法,EventBus的工作機制大概可以分為圖中三部分,即注冊,發送和調度三個步驟,其中構建部分主要是幾個數據結構用來記錄信息,后面三個則是三個步驟,圖中總結了其中最主要的方法調用。
后記
EventBus可以很方便地在不同對象之間傳遞消息或者對象,并且很容易做到線程的切換,但是它也有它的局限性,就是不適用于多進程的應用,在多進程的情況下,EventBus中無論是單例還是同步方法以及同步塊都會失效,所以便會失去了傳遞消息的作用。