EventBus源碼分析(三)

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中,注冊方法被調用的線程分別為:

  1. POSTING:最簡單的一種模式,即直接在事件發送的線程中調用注冊方法;
  2. MAIN:在主線程中調用注冊方法,如果事件發送線程不是主線程,則由mainThreadPoster將方法調用派送到主線程中調用,其實就是使用的Handler機制,后面再做分析;
  3. BACKGROUND:在后臺線程中調用,如果事件發送不是在主線程,則注冊方法則直接在該線程中被調用,如果在主線程發送事件,則注冊方法由backgroundPoster派發到工作線程中調用;
  4. 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.png

后記

EventBus可以很方便地在不同對象之間傳遞消息或者對象,并且很容易做到線程的切換,但是它也有它的局限性,就是不適用于多進程的應用,在多進程的情況下,EventBus中無論是單例還是同步方法以及同步塊都會失效,所以便會失去了傳遞消息的作用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容