事件總線框架:EventBus 實現原理

一、簡單使用

依賴

implementation 'org.greenrobot:eventbus:3.2.0'

Demo

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 注冊訂閱者
        EventBus.getDefault().register(this);

        // 測試按鈕
        findViewById(R.id.tv).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 事件發布者(Publisher):用于通知 Subscriber 有事件發生??梢栽谌我饩€程任 意位置發送事件。
                EventBus.getDefault().post(new ToastEvent("123"));
            }
        });

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 必須要回收 否則會內存泄漏
        EventBus.getDefault().unregister(this);
    }


    /**
     * 事件訂閱者(Subscriber):接收特定的事件。
     *
     * @param event 傳遞的事件
     */
    @Subscribe
    public void receiveToast(ToastEvent event) {
        Toast.makeText(MainActivity.this, "測試點擊事件   --> " + event.getMsg(), Toast.LENGTH_SHORT).show();
    }


    /**
     * 事件(Event):可以是任意類型的對象。通過事件的發布者將事件進行傳遞。
     */
    public static class ToastEvent {
        private String msg;

        public ToastEvent(String msg) {
            this.msg = msg;
        }

        public String getMsg() {
            return msg;
        }

        public void setMsg(String msg) {
            this.msg = msg;
        }
    }

}


二、EventBus簡介

EventBus是一個Android端優化的publish/subscribe消息總線,簡化了應用程序內各組件間、組件與后臺線程間的通信。

作為一個消息總線主要有三個組成部分:

  • 事件(Event):可以是任意類型的對象。通過事件的發布者將事件進行傳遞。
  • 事件訂閱者(Subscriber):接收特定的事件。
  • 事件發布者(Publisher):用于通知 Subscriber 有事件發生。可以在任意線程任 意位置發送事件。

上圖解釋了整個EventBus的大概工作流程:事件的發布者(Publisher)將事件 (Event)通過post()方法發送。EventBus內部進行處理,找到訂閱了該事件 (Event)的事件訂閱者(Subscriber)。然后該事件的訂閱者(Subscriber)通過 onEvent()方法接收事件進行相關處理(關于onEvent()在EventBus 3.0中有改動, 下面詳細說明)。

三、EventBus的進階使用

1.線程模式ThreadMode

當你接收的的事件后,如果處于非UI線程,你要更新UI怎么辦?如果處于UI線程,
你要進行耗時操作,怎么辦?等等其他情況,通過ThreadMode統統幫你解決。

    @Subscribe(threadMode = ThreadMode.MainThread)
    public void onNewsEvent(NewsEvent event) {
        String message = event.getMessage();
        mTv_message.setText(message);
    }

PostThread:
事件的處理在和事件的發送在相同的進程,所以事件處理時間不應 太長,不然影響事件的發送線程。
MainThread:
事件的處理會在UI線程中執行。事件處理時間不能太長,這個不用說 的,長了會ANR的。
BackgroundThread:
如果事件是在UI線程中發布出來的,那么事件處理就會在子 線程中運行,如果事件本來就是子線程中發布出來的,那么事件處理直接在該子線 程中執行。所有待處理事件會被加到一個隊列中,由對應線程依次處理這些事件, 如果某個事件處理時間太長,會阻塞后面的事件的派發或處理。
Async:
事件處理會在單獨的線程中執行,主要用于在后臺線程中執行耗時操作, 每個事件會開啟一個線程。

2.priority事件優先級

事件的優先級類似廣播的優先級,優先級越高優先獲得消息。 用法展示:

    @Subscribe(priority = 100,threadMode = ThreadMode.MAIN)
    public void onToastEvent(ToastEvent event) {
         Toast.makeText(MainActivity.this, event.getMsg(), Toast.LENGTH_SHORT).show();
    }

當多個訂閱者(Subscriber)對同一種事件類型進行訂閱時,即對應的事件處理方 法中接收的事件類型一致,則優先級高(priority 設置的值越大),則會先接收事 件進行處理;優先級低(priority 設置的值越小),則會后接收事件進行處理。

除此之外,EventBus也可以終止對事件繼續傳遞的功能。 用法展示:

    @Subscribe(priority = 100, threadMode = ThreadMode.MAIN)
    public void onToastEvent(ToastEvent event) {
        Toast.makeText(MainActivity.this, event.getMsg(), Toast.LENGTH_SHORT).show();
        // 取消后續的事件分發
        EventBus.getDefault().cancelEventDelivery(event);
    }

這樣其他優先級比100低,并且訂閱了該事件的訂閱者就會接收不到該事件。

3.EventBus黏性事件

EventBus除了普通事件也支持粘性事件。可以理解成:訂閱在發布事件之后,但同 樣可以收到事件。訂閱/解除訂閱和普通事件一樣,但是處理訂閱的方法有所不同, 需要注解中添加sticky = true。 用法展示:

    @Subscribe(priority = 100, threadMode = ThreadMode.MAIN, sticky = true)
    public void onToastEvent(ToastEvent event) {
        Toast.makeText(MainActivity.this, event.getMsg(), Toast.LENGTH_SHORT).show();
        // 取消后續的事件分發
        EventBus.getDefault().cancelEventDelivery(event);
    }

這樣,假設一個ToastEvent 的事件已經發布,此時還沒有注冊訂閱。當設置了 sticky = true,在ToastEvent 的事件發布后,進行注冊。依然能夠接收到之前發布 的事件。

不過這個時候,發布事件的方式就改變了。

EventBus.getDefault().postSticky(new ToastEvent("Toast,發個提示, 祝大家新年快樂!"));

我們如果不再需要該粘性事件我們可以移除

 EventBus.getDefault().removeStickyEvent(ToastEvent.class);

或者調用移除所有粘性事件

 EventBus.getDefault().removeAllStickyEvents();

四、源碼解析

getDefault()

    public static EventBus getDefault() {
        EventBus instance = defaultInstance;
        if (instance == null) {
            synchronized (EventBus.class) {
                instance = EventBus.defaultInstance;
                if (instance == null) {
                    instance = EventBus.defaultInstance = new EventBus();
                }
            }
        }
        return instance;
    }

通過上述代碼可以得知,getDefault()中通過雙檢查鎖(DCL)機制實現了 EventBus的單例機制,獲得了一個默認配置的EventBus對象。 下面我們繼續看 register()方法。

register()

在了解register()之前,我們先要了解一下EventBus中的幾個關鍵的成員變量。方便 對下面內容的理解。

/** Map<訂閱事件, 訂閱該事件的訂閱者集合> */
private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
/** Map<訂閱者, 訂閱事件集合> */
private final Map<Object, List<Class<?>>> typesBySubscriber;
/** Map<訂閱事件類類型,訂閱事件實例對象>. */
private final Map<Class<?>, Object> stickyEvents;

下面看具體的register()中執行的代碼。

    public void register(Object subscriber) {
        //訂閱者類型
        Class<?> subscriberClass = subscriber.getClass();
        //獲取訂閱者全部的響應函數信息(即上面的onNewsEvent()之類的方法)
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
        synchronized (this) {
            //循環每一個事件響應函數,執行 subscribe()方法,更新訂閱相關信息
            for (SubscriberMethod subscriberMethod : subscriberMethods) {
                subscribe(subscriber, subscriberMethod);
            }
        }
    }

由此可見,register()第一步獲取訂閱者的類類型. 第二步,通過 SubscriberMethodFinder類來解析訂閱者類,獲取所有的響應函數集合. 第三步,遍歷 訂閱函數,執行 subscribe()方法,更新訂閱相關信息。 關于 subscriberMethodFinder這里就不介紹了。先跟著線索,繼續看subscribe()方法。 subscribe 函數分三步。

    // Must be called in synchronized block
    private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
        // 第一步 
        // 獲取訂閱的事件類型
        Class<?> eventType = subscriberMethod.eventType;
        Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
        CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        if (subscriptions == null) {
            subscriptions = new CopyOnWriteArrayList<>();
            subscriptionsByEventType.put(eventType, subscriptions);
        } else {
            if (subscriptions.contains(newSubscription)) {
                throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
                        + eventType);
            }
        }

        int size = subscriptions.size();
        for (int i = 0; i <= size; i++) {
            if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
                subscriptions.add(i, newSubscription);
                break;
            }
        }

        // 第二步
        //根據訂閱者,獲得該訂閱者訂閱的事件類型集合
        List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
        //如果事件類型集合為空,創建新的集合,并加入新訂閱的事件類型。
        if (subscribedEvents == null) {
            subscribedEvents = new ArrayList<>();
            typesBySubscriber.put(subscriber, subscribedEvents);
        }
        //如果事件類型集合不為空,加入新訂閱的事件類型
        subscribedEvents.add(eventType);

        // 第三步
        //該事件是stick=true。
        if (subscriberMethod.sticky) {
            //響應訂閱事件的父類事件
            if (eventInheritance) {
                Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();
                //循環獲得每個stickyEvent事件
                for (Map.Entry<Class<?>, Object> entry : entries) {
                    Class<?> candidateEventType = entry.getKey();
                    //是該類的父類
                    if (eventType.isAssignableFrom(candidateEventType)) {
                        //該事件類型最新的事件發送給當前訂閱者。
                        Object stickyEvent = entry.getValue();
                        checkPostStickyEventToSubscription(newSubscription, stickyEvent);
                    }
                }
            } else {
                Object stickyEvent = stickyEvents.get(eventType);
                checkPostStickyEventToSubscription(newSubscription, stickyEvent);
            }
        }
    }
  • 由此可見,第一步:通過subscriptionsByEventType得到該事件類型所有訂閱者信 息隊列,根據優先級將當前訂閱者信息插入到訂閱者隊列 subscriptionsByEventType中;
  • 第二步:在typesBySubscriber中得到當前訂閱者訂閱的所有事件隊列,將此事件保 存到隊列typesBySubscriber中,用于后續取消訂閱;
  • 第三步:檢查這個事件是否 是 Sticky 事件,如果是則從stickyEvents事件保存隊列中取出該事件類型最后一個 事件發送給當前訂閱者。
    到此,便完成了訂閱功能。下面是訂閱的具體流程圖:

unregister()

    public synchronized void unregister(Object subscriber) {
        // 獲取該訂閱者所有的訂閱事件類類型集合.
        List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
        if (subscribedTypes != null) {
            for (Class<?> eventType : subscribedTypes) {
                unsubscribeByEventType(subscriber, eventType);
            }
            // 從typesBySubscriber刪除該<訂閱者對象,訂閱事件類類型集合>
            typesBySubscriber.remove(subscriber);
        } else {
            logger.log(Level.WARNING, "Subscriber to unregister was not registered before: " + subscriber.getClass());
        }
    }

    private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {
        // 獲取訂閱事件對應的訂閱者信息集合.
        List<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        if (subscriptions != null) {
            int size = subscriptions.size();
            for (int i = 0; i < size; i++) {
                Subscription subscription = subscriptions.get(i);
                // 從訂閱者集合中刪除特定的訂閱者.
                if (subscription.subscriber == subscriber) {
                    subscription.active = false;
                    subscriptions.remove(i);
                    i--;
                    size--;
                }
            }
        }
    }

unregister()方法比較簡單,主要完成了subscriptionsByEventType以及
typesBySubscriber兩個集合的同步。

post()

    public void post(Object event) {
        PostingThreadState postingState = currentPostingThreadState.get();
        List<Object> eventQueue = postingState.eventQueue;
        //將當前事件添加到其事件隊列
        eventQueue.add(event);
        //判斷新加入的事件是否在分發中
        if (!postingState.isPosting) {
            postingState.isMainThread = isMainThread();
            postingState.isPosting = true;
            if (postingState.canceled) {
                throw new EventBusException("Internal error. Abort state was not reset");
            }
            try {
                // 循環處理當前線程eventQueue中的每一個event對象.
                while (!eventQueue.isEmpty()) {
                    postSingleEvent(eventQueue.remove(0), postingState);
                }
            } finally {
                // 處理完知乎重置postingState一些標識信息.
                postingState.isPosting = false;
                postingState.isMainThread = false;
            }
        }
    }

post 函數會首先得到當前線程的 post 信息PostingThreadState,其中包含事件隊 列,將當前事件添加到其事件隊列中,然后循環調用 postSingleEvent 函數發布隊 列中的每個事件。

總結

1、要理解EventBus就要從register,unRegister,post,postSticky方法入手。要理解register實質上是將訂閱對象(比如activity)中的每個帶有subscriber的方法找出來,最后獲得調用的就是這些方法。訂閱對象(比如activity)是一組event方法的持有者。

2、后注冊的對象中sticky方法能夠收到之前的stickyEvent方法的原因是EventBus中維護了stickyEvent的hashMap表,在subsribe注冊的時候就遍歷其中有沒有注冊監聽stickyEvent如果有就會執行一次回調。

EventBus缺點

1、使用的時候有定義很多event類
2、event在注冊的時候會調用反射去遍歷注冊對象的方法在其中找出帶有@subscriber標簽的方法,性能不高。
3、需要自己注冊和反注冊,如果忘了反注冊就會導致內存泄漏

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