EventBus源碼分析(三)

EventBus源碼分析(三)

在前面的兩篇文章中分別對EventBus中的構(gòu)建刺彩,分發(fā)事件以及注冊方法的查詢做了分析,本篇文章是最后一部分介粘,主要針對EventBus對于注冊方法的調(diào)用作學(xué)習(xí)了解沟涨,前面兩篇文章的鏈接如下:
EventBus源碼分析(一)
EventBus源碼分析(二)

EventBus方法調(diào)用機(jī)制需要從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情況下,注冊方法在不同線程調(diào)用的情況桨仿。
在理解注冊方法在哪個線程被調(diào)用之前需要首先明確postToSubscription()方法在哪個線程被調(diào)用睛低,該方法是由EventBus對象的post()方法經(jīng)過一系列方法調(diào)用的,所以它運(yùn)行在用戶發(fā)送事件的線程中服傍,參數(shù)isMainThread表示該運(yùn)行線程是否為主線程钱雷,下面四種threadMode中,注冊方法被調(diào)用的線程分別為:

  1. POSTING:最簡單的一種模式吹零,即直接在事件發(fā)送的線程中調(diào)用注冊方法罩抗;
  2. MAIN:在主線程中調(diào)用注冊方法,如果事件發(fā)送線程不是主線程灿椅,則由mainThreadPoster將方法調(diào)用派送到主線程中調(diào)用套蒂,其實(shí)就是使用的Handler機(jī)制钞支,后面再做分析;
  3. BACKGROUND:在后臺線程中調(diào)用操刀,如果事件發(fā)送不是在主線程烁挟,則注冊方法則直接在該線程中被調(diào)用,如果在主線程發(fā)送事件骨坑,則注冊方法由backgroundPoster派發(fā)到工作線程中調(diào)用撼嗓;
  4. ASYNC:直接由asyncPoster派發(fā)到工作線程中調(diào)用,至于它與BACKGROUND的區(qū)別欢唾,后面會根據(jù)源碼做分析且警。

所以從該方法中可以看出,在切換不同線程調(diào)用注冊方法礁遣,是通過三個Poster完成的斑芜,下面分別對它們的源碼做分析。

1. HandlerPoster

首先mainThreadPoster的類型為HandlerPoster亡脸,從名字可以看出它應(yīng)該與Handler有關(guān)押搪。在android的編程中,如果一些耗時(shí)操作的結(jié)果需要更新UI浅碾,那么需要通過定義Handler的形式大州,將結(jié)果加入到主線程的消息隊(duì)列,然后由自定義在主線程的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,那么它則是根據(jù)構(gòu)造器中傳入的Looper對象滥朱,在handleMessage()方法中處理對應(yīng)線程中消息隊(duì)列的消息根暑,那么我們再看在EventBus對象初始化時(shí),mainThreadPoster初始化是否為主線程的Looper, EventBus的構(gòu)造器中的部分代碼為:

mainThreadPoster = new HandlerPoster(this, Looper.getMainLooper(), 10);

Looper.getMainLooper()說明了mainThreadPoster的handleMessage方法是在主線程中被調(diào)用徙邻,就可以實(shí)現(xiàn)將注冊方法派發(fā)到主線程中調(diào)用了排嫌。其他參數(shù)后面部分一一分析。
首先來看PendingPostQueue這個明顯是一個隊(duì)列缰犁,其代碼有興趣的可以自行查看淳地,它是自定義的隊(duì)列,沒有實(shí)現(xiàn)Java定義的Queue接口帅容,這里只是簡單定義了內(nèi)部的隊(duì)列數(shù)據(jù)結(jié)構(gòu)以及入隊(duì)和出隊(duì)方法颇象,只不過有一個與時(shí)間相關(guān)的出隊(duì)方法,后面用到時(shí)會提到并徘。PendingPostQueue內(nèi)部的隊(duì)列結(jié)構(gòu)中的元素即為PendingPost遣钳,它封裝了一次注冊方法調(diào)用所需要的注冊信息和事件類型,即Subscription類型對象和事件對象麦乞。類的代碼很簡單蕴茴,不再貼出劝评,但是PendingPost中有一個對象的緩存池,最多可以保存10000個PendingPost對象荐开,避免了對象的創(chuàng)建和回收付翁。有興趣的可以查看源碼。

下面看HandlerPoster的入隊(duì)方法:

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");
            }
        }
    }
}

首先根據(jù)注冊信息和事件對象構(gòu)建一個PendingPost加入隊(duì)列晃听,然后通過發(fā)送消息通知Handler自身調(diào)用handleMessage()方法百侧,這里的handlerActive是一個標(biāo)志位,標(biāo)志當(dāng)前是否正在執(zhí)行處理PendingPostQueue隊(duì)列中的PendingPost能扒,也就是正在調(diào)用隊(duì)列中的注冊方法佣渴。下面為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;
    }
}

這里代碼的邏輯就是循環(huán)遍歷隊(duì)列,通過eventBus.invokeSubscriber(pendingPost)方法執(zhí)行注冊方法初斑,invokeSubscriber()方法在第一篇文章中已經(jīng)分析過辛润。兩次判斷pendingPost是否為空是處理多線程的同步問題,這里與單例模式是同樣的道理见秤。不過這里需要注意的是EventBus為HandlerPoster處理調(diào)用注冊方法設(shè)定了時(shí)間上限砂竖,也就是構(gòu)造器中初始化的maxMillisInsideHandleMessage,在EventBus中初始化mainThreadPost時(shí)為其指定了10ms, 也就是while循環(huán)超過10ms之后會退出handleMessage方法鹃答,并將handleActive置位true乎澄,并再次發(fā)送消息,使得該handler再次調(diào)用handleMessage()方法测摔,繼續(xù)處理隊(duì)列中的注冊信息置济,這是為了避免隊(duì)列過長是,while循環(huán)阻塞主線程造成卡頓锋八。
以上就是HandlerPoster的全部代碼浙于,十分簡短,通過Handler的方式將注冊方法的調(diào)用派發(fā)到其他線程挟纱,在EventBus中只是將其用于在主線程中調(diào)用注冊方法的情況羞酗。

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,是一個可執(zhí)行的任務(wù)紊服,這里看到BackgroundPoster中也有隊(duì)列和標(biāo)志位檀轨,其執(zhí)行邏輯與HandlerPoster應(yīng)該極為相似,下面看一下它的入隊(duì)方法:

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);
        }
    }
}

代碼邏輯很簡單围苫,構(gòu)建PendingPost裤园,并在同步塊中將該對象入隊(duì)撤师,然后調(diào)用線程池中的線程執(zhí)行該Runnable剂府,即在其他線程中執(zhí)行run方法,進(jìn)而調(diào)用注冊方法剃盾,下面看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循環(huán)腺占,除了沒有設(shè)定時(shí)間上限之外淤袜,幾乎與HandlerPoster中handleMessage方法中的處理邏輯完全一致,只不過標(biāo)志位改成了executorRunning衰伯,其作用也幾乎是一致的铡羡。但是這里需要注意有另外一個與時(shí)間有關(guān)的方法,即queue.poll(1000)意鲸,這里有一個等待過程烦周,即從隊(duì)列中取PendingPost對象時(shí),如果沒有PendingPost會等待1000毫秒怎顾,其代碼如下:

synchronized PendingPost poll(int maxMillisToWait) throws InterruptedException {
    if (head == null) {
        wait(maxMillisToWait);
    }
    return poll();
}

這里是典型的生產(chǎn)者和消費(fèi)者模型读慎,并且設(shè)定了時(shí)間上限。因此也就說明了BackgroundPoster會將1000毫秒以內(nèi)入隊(duì)的注冊信息在同一個線程中調(diào)用槐雾,這一點(diǎn)也是與AsyncPoster最重要的區(qū)別夭委。BackgroundPoster的代碼也介紹完了,同樣十分簡單募强,下面繼續(xù)分析AsyncPoster中的代碼株灸,下面你會看到,其實(shí)現(xiàn)更簡單擎值。

3. AsyncPoster

由于AsyncPoster的代碼實(shí)現(xiàn)比較簡單慌烧,這里一次性全部貼出它的代碼:

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,但是可以看到這里沒有了標(biāo)志位和while循環(huán)幅恋,入隊(duì)的邏輯就是構(gòu)建PendingPost杏死,然后加入隊(duì)列,這里注意enqueue是同步的方法捆交,不用擔(dān)心同步問題淑翼,然后調(diào)用線程池的線程執(zhí)行該Runnable,run方法的邏輯就是取出隊(duì)列的pendingPost, 并執(zhí)行invokeSubscriber方法品追,由于是入隊(duì)一次調(diào)用一次execute方法玄括,所以正常情況下隊(duì)列中不會取不出pendingPost, 也不會剩余pendingPost。
這里AsyncPoster更為簡單肉瓦,但是重點(diǎn)是需要思考一下它與BackgroundPoster的區(qū)別遭京,前面也提到過BackgroundPoster設(shè)定一個時(shí)間上限,并且在該段時(shí)間內(nèi)入隊(duì)的注冊信息會在同一個線程中調(diào)用泞莉,而AsyncPoster則是完全異步的調(diào)用哪雕。也就是說BackgroundPoster會盡可能在同一線程中調(diào)用注冊方法,但不保證鲫趁,可是它保證有一定的順序斯嚎,也就是注冊信息收集的順序,而AsyncPoster對注冊方法的調(diào)用則是完全異步,不確定在哪個線程堡僻,也不確定順序糠惫。

至此,對于注冊方法調(diào)用的線程調(diào)度分析源碼已經(jīng)分析完了钉疫,總結(jié)起來只是三個不同的Poster, HandlerPoster是繼承自Handler, 通過handler機(jī)制可以將線程調(diào)度分派給主線程硼讽,而BackgroundPoster和AsyncPoster則是繼承自Runnable,覆寫run方法定義調(diào)用注冊方法的任務(wù)牲阁,并有線程池負(fù)責(zé)調(diào)度啟動該任務(wù)固阁,進(jìn)而完成對注冊方法的調(diào)用。

通過三篇文章城菊,對EventBus的源碼大致分析了一遍您炉,下圖對分析的流程大致做一個總結(jié),EventBus主要負(fù)責(zé)不同對象間傳遞消息役电,并且可以很方便地切換線程調(diào)用注冊方法赚爵,EventBus的工作機(jī)制大概可以分為圖中三部分,即注冊法瑟,發(fā)送和調(diào)度三個步驟冀膝,其中構(gòu)建部分主要是幾個數(shù)據(jù)結(jié)構(gòu)用來記錄信息,后面三個則是三個步驟霎挟,圖中總結(jié)了其中最主要的方法調(diào)用窝剖。

eventbus.png

后記

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末讶隐,一起剝皮案震驚了整個濱河市起胰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌巫延,老刑警劉巖效五,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異炉峰,居然都是意外死亡畏妖,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門疼阔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來戒劫,“玉大人适瓦,你說我怎么就攤上這事∑滓牵” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵否彩,是天一觀的道長疯攒。 經(jīng)常有香客問我,道長列荔,這世上最難降的妖魔是什么敬尺? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮贴浙,結(jié)果婚禮上砂吞,老公的妹妹穿的比我還像新娘。我一直安慰自己崎溃,他們只是感情好蜻直,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著袁串,像睡著了一般概而。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上囱修,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天赎瑰,我揣著相機(jī)與錄音,去河邊找鬼破镰。 笑死餐曼,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鲜漩。 我是一名探鬼主播源譬,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼孕似!你這毒婦竟也來了瓶佳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鳞青,失蹤者是張志新(化名)和其女友劉穎霸饲,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體臂拓,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡厚脉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了胶惰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片傻工。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出中捆,到底是詐尸還是另有隱情鸯匹,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布泄伪,位于F島的核電站殴蓬,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蟋滴。R本人自食惡果不足惜染厅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望津函。 院中可真熱鬧肖粮,春花似錦、人聲如沸尔苦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽允坚。三九已至凌净,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間屋讶,已是汗流浹背冰寻。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留皿渗,地道東北人斩芭。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像乐疆,于是被迫代替她去往敵國和親划乖。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容