Android 註解系列之 EventBus3 原理(四)

AndyJennifer發表於2019-10-22

前言

在之前的文章 Android 註解系列之APT工具(三) 中,我們介紹了 APT 技術的及其使用方式,也提到了一些知名的開源框架如 Dagger2ButterKnifeEventBus 都使用了該技術。為了讓大家更好的瞭解 APT 技術的使用,在接下來的文章中我將會著重帶領大家來了解 EventBus 中 APT 技術的使用,在瞭解該知識之前,需要我們對 EventBus 內部原理較為熟悉,如果你已經熟悉其內部機制了,可以跳過該篇文章,直接閱讀 Android 註解系列之EventBus3 “加速引擎“(五)

閱讀該篇文章,我們能夠學到如下知識點:

  • EventBus3 內部原理
  • EventBus3 訂閱與傳送訊息原理
  • EventBus3 執行緒切換的原理
  • EventBus3 粘性事件的處理

整篇文章結合 EventBus 3.1.1 版本進行講解。

EventBus 簡介

EventBus 對於 Android 程式設計師來說應該不是很陌生,它是基於觀察者模式的事件釋出/訂閱框架,我們常常用它來實現不同元件的通訊,後臺執行緒通訊等。

EventBus-Publish-Subscribe.png

雖然 EventBus 非常簡單好用,但是還是會因為 EventBus 滿天飛,使程式程式碼結構非常混亂,難以測試和追蹤。即使 EventBus 有很多詬病,但仍然不影響我們去學習其中的原理與程式設計思想~

大概流程

在瞭解 EventBus 內部原理之前,我們先了解一下 EventBus 框架的一個大概流程。如下圖所示:

EventBus粗暴理解.jpg

上圖中綠色為訂閱流程,紅色為傳送事件流程,大家可以結合上圖,來理解原始碼。

在上圖中我們在 A.java 中訂閱了事件 AEvent,在 B.java 中訂閱了事件 AEventBEvent,下面我們來分析 EventBus 中註冊與事件傳送的兩個流程,在介紹兩個流程之前,先介紹一下 SubscriptionSubscriberMethod 中所包含的內容。

Subscription 類中包含以下內容:

  • 當前註冊物件
  • 對應訂閱方法的封裝物件 SubscriberMethod

SubscriberMethod 類中包含以下內容:

  • 包含 @Subscribe 註解的方法的 Method (java.lang.reflect 包下的物件)。
  • @Subscribe 註解中設定的執行緒模式 ThreadMode
  • 方法的註冊的事件型別的 Class 物件
  • @Subscribe中設定的優先順序 priority
  • @Subscribe中設定事件是否是粘性事件 sticky

註冊流程

當我們通過呼叫 EventBus.register() 註冊 A、B 兩個物件時,EventBus 會做以下幾件事件:

  • 通過內部的 SubscriberMethodFinder 來獲取 A、B類中含有 @Subscribe 註解的方法,並將該註解中的內容與對應方法封裝為 SubscriberMethod 物件。然後再將當前訂閱物件與對應的 SubscriberMethod 再封裝為 Subscription 物件。
  • 將所有的 Subscription 放在名為 subscriptionsByEventType 型別為 Map<Class<?>, CopyOnWriteArrayList<Subscription>> 資料結構(key 為事件型別的 Class 物件) 中,因為 Subscription 物件內部包含 SubscriberMethod, 那麼就能知道訂閱的事件型別,所以我們可以根據事件型別來區分 Subscription ,又因為相同事件可以被不同訂閱者中的方法來訂閱,所以相同型別的事件也就以對應不同的 Subscription
  • 將訂閱者中的所有訂閱的事件都封裝在名為 typesBySubscriber 型別為 Map<Object, List<Class<?>>>資料結構(key 為訂閱物件,value 為該物件訂閱的事件型別 Class 物件)。該集合主要用於取消訂閱,在下文中我們會進行介紹。

在整個註冊流程中,最主要的流程就是 EventBus 通過 SubscriberMethodFinder 去獲取類中包含 @Subscribe 註解的訂閱方法。在 EventBus 3.0 之前該流程一直都是通過反射的方式去獲取。在 3.0 及以後版本,EventBus 採用了 APT 技術,對 SubscriberMethodFinder 查詢訂閱方法流程進行了優化,使其能在 EventBus.register() 方法呼叫之前就能知道相關訂閱事件的方法,這樣就減少了程式在執行期間使用反射遍歷獲取方法所帶來的時間消耗。在下文中我們也會指出具體的優化點。

事件傳送流程

知道了 EventBus 的註冊過程,再來了解事件的發生流程就非常簡單了。因為我們已經通過 subscriptionsByEventType 儲存事件對應的 Subscription,只要找到了 Subscription ,那麼我們就能從 Subscription 拿到訂閱事件的物件 subscriber ,以及對應的訂閱方法 Method (java.lang.reflect 包下的物件)。然後通過反射呼叫:

Subscription 內部包含訂閱者及 SubscriberMethod(內部包含訂閱方法 Method )

 method.invoke(subscription.subscriber, event)
複製程式碼

通過上述方法,就能將對應事件傳送到相關訂閱者了。當然這裡只是簡單的介紹了事件是如何傳送到相關訂閱者的。關於 EventBus 中粘性事件的處理,執行緒如何切換。會在下文中進行詳細介紹。

原始碼分析

在瞭解了 EventBus 的內部大概流程後,現在我們通過原始碼來更深層次的瞭解其內部實現。還是從訂閱過程與事件的傳送兩個過程進行講解。

訂閱過程原始碼分析

EventBus 的訂閱入口為 register() 方法,如下所示:

  public void register(Object subscriber) {
        Class<?> subscriberClass = subscriber.getClass();
        //流程1:獲取對應類中所有的訂閱方法
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
        synchronized (this) {
            //流程2:實際訂閱
            for (SubscriberMethod subscriberMethod : subscriberMethods) {
                subscribe(subscriber, subscriberMethod);
            }
        }
    }
複製程式碼

在該方法中,主要涉及到 SubscriberMethodFinder 查詢方法與實際訂閱兩個流程,下面我們會對這兩個流程進行介紹。

SubscriberMethodFinder 查詢方法流程

在該流程中,主要通過 SubscriberMethodFinder 去獲取訂閱者中所有的 SubscriberMethod ,我們先看 findSubscriberMethods() 方法:

    List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
        //從快取中獲取訂閱者中的訂閱方法,如果有則讀快取,如果沒有進行查詢
        List<SubscriberMethod> subscriberMethods = (List)METHOD_CACHE.get(subscriberClass);
        if (subscriberMethods != null) {
            return subscriberMethods;
        } else {
            if (this.ignoreGeneratedIndex) {//如果忽略索引類,則使用反射。
                subscriberMethods = this.findUsingReflection(subscriberClass);
            } else {//否則使用索引類
                subscriberMethods = this.findUsingInfo(subscriberClass);
            }
            //如果訂閱者沒有訂閱方法,則丟擲異常
            if (subscriberMethods.isEmpty()) {
                throw new EventBusException("Subscriber " + subscriberClass + " and its super classes have no public methods with the @Subscribe annotation”);
            } else {
                //將對應類中的訂閱方法,新增到快取中,提高效率,方便下次查詢
                METHOD_CACHE.put(subscriberClass, subscriberMethods);
                return subscriberMethods;
            }
        }
    }
複製程式碼

該方法的邏輯也非常簡單,為如下幾個步驟:

  • 步驟1:先從快取( METHOD_CACHE )中獲取訂閱者對應的 SubscriberMethod(訂閱方法) ,如果有則從快取中取。
  • 步驟2:如果快取中沒有,則通過布林變數 ignoreGeneratedIndex,來判斷是直接使用反射獲取訂閱方法,還是通過索引類(EventBus 3.0 使用APT 增加的類)來獲取。因為 ignoreGeneratedIndex 預設值為 false ,則預設會走 findUsingInfo() 方法
  • 步驟3:將步驟2中獲得的訂閱方法集合,儲存到快取中,方便下一次獲取,提高效率。

因為預設會走 findUsingInfo() 方法,我們繼續檢視該方法:

    private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
        //步驟1:構建了查詢狀態快取池,最多快取4個類的查詢狀態
        FindState findState = prepareFindState();
        findState.initForSubscriber(subscriberClass);
        while (findState.clazz != null) {
            //步驟2,獲取查詢狀態對應的訂閱資訊,?這裡EventBus 3.0 使用了索引類,
            findState.subscriberInfo = getSubscriberInfo(findState);
            if (findState.subscriberInfo != null) {
                SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
                for (SubscriberMethod subscriberMethod : array) {
                    //將訂閱者的所有的訂閱方法新增到FindState的集合中
                    if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
                        findState.subscriberMethods.add(subscriberMethod);
                    }
                }
            } else {//步驟3:如果訂閱資訊為null,則通過反射來獲取類中所有的方法
                findUsingReflectionInSingleClass(findState);
            }// 繼續查詢父類的方法
            findState.moveToSuperclass();
        }
        //步驟4,獲取findState中的所有方法,並清空物件池
        return getMethodsAndRelease(findState);
    }
複製程式碼
  • 步驟1:建立與訂閱者相關的 FindState 物件。會從 FinState 物件快取池(最大為4個)中獲取,一個訂閱者物件對應一個FindState,一個訂閱者物件對應一個或多個訂閱方法。
  • 步驟2:通過 FindState 物件 呼叫 getSubscriberInfo() 方法去獲取訂閱者相關的訂閱方法資訊。該方法使用了 APT 技術,構建了EventBus的索引類。關於具體的優化,會在下篇文章中Android 註解系列之EventBus3“加速引擎“(五)進行描述,大家這裡有個印象就好了。
  • 步驟3:如果通過步驟2獲取不到訂閱方法資訊,則通過反射來獲取類中的所有的訂閱方法。並將獲取的方法,封裝到 FindState 中的 subscriberMethods 集合中去。
  • 步驟4:將 FindState 物件中的 subscriberMethods 集合返回。

在上述方法中,我們需要注意的是,如果當前訂閱著沒有相關的訂閱方法,那麼會依次遍歷其父類的訂閱方法。還有一個知識點,就是該方法中 FindState 使用了 物件快取池,不會每次註冊一個訂閱者就建立 一個FindState 物件。這樣就節約了記憶體的使用。

關於索引類的知識點,會在下篇文章中進行介紹,這裡我們直接檢視 findUsingReflectionInSingleClass() 方法:

  private void findUsingReflectionInSingleClass(FindState findState) {
        Method[] methods;
        try {
             //獲取當前訂閱者中的所有的方法
            methods = findState.clazz.getDeclaredMethods();
        } catch (Throwable th) {
            //獲取該類的所有public 方法 包括繼承的公有方法
            methods = findState.clazz.getMethods();
            findState.skipSuperClasses = true;
        }
        //迴圈遍歷所有的方法,通過相關注解找到相應的訂閱方法。
        for (Method method : methods) {
            int modifiers = method.getModifiers();
            //滿足修飾符為 public 並且非抽象、非靜態
            if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
                Class<?>[] parameterTypes = method.getParameterTypes();
                //找到引數為1,且該方法包含Subscrile註解的方法
                if (parameterTypes.length == 1) {
                    Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
                    if (subscribeAnnotation != null) {
                        Class<?> eventType = parameterTypes[0];
                        if (findState.checkAdd(method, eventType)) {
                            // 建立訂閱方法物件,並將對應方法物件,事件型別,執行緒模式,優先順序,粘性事件封裝到SubscriberMethod物件中。
                            ThreadMode threadMode = subscribeAnnotation.threadMode();
                            findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
                                    subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
                        }
                    }
                } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
                    String methodName = method.getDeclaringClass().getName() + "." + method.getName();
                    throw new EventBusException("@Subscribe method " + methodName +
                            "must have exactly 1 parameter but has " + parameterTypes.length);
                }
            } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
                String methodName = method.getDeclaringClass().getName() + "." + method.getName();
                throw new EventBusException(methodName +
                        " is a illegal @Subscribe method: must be public, non-static, and non-abstract”);
            }
        }
    }
複製程式碼

該方法的邏輯也非常簡單,通過獲取 FindState 中的訂閱者的 Class 物件,然後通過反射獲取所有包含 @Subscribe 註解且引數為 1 的 Method 物件,並讀取到該引數的型別EventType,接著讀取註解中的 thredModeprioritysticy,最後將這些資料都統一分裝到新建的SubscriberMethod 物件中,最後將該物件新增到 FindState 中的 subscriberMethods 集合中去。

實際訂閱方法 subscribe

當找到訂閱者所有的方法集合後,最終會遍歷呼叫 subscribe() 方法,檢視該方法:

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
        Class<?> eventType = subscriberMethod.eventType;

        //步驟1,將每個訂閱方法和訂閱者封裝成Subscription
        Subscription newSubscription = new Subscription(subscriber, subscriberMethod);

        //步驟2,獲取對應事件中所有的 Subscription,判斷是否重複新增
        CopyOnWriteArrayList<Subscription> subscriptions = (CopyOnWriteArrayList)this.subscriptionsByEventType.get(eventType);
        if (subscriptions == null) {
            subscriptions = new CopyOnWriteArrayList();
            this.subscriptionsByEventType.put(eventType, subscriptions);
        } else if (subscriptions.contains(newSubscription)) {
            throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event " + eventType);
        }

        //步驟3,根據優先順序,將當前新封裝的Subscription物件新增到subscriptionsByEventType中去
        int size = subscriptions.size();
        for(int i = 0; i <= size; ++i) {
            if (i == size || subscriberMethod.priority > ((Subscription)subscriptions.get(i)).subscriberMethod.priority) {
                subscriptions.add(i, newSubscription);
                break;
            }
        }

       //步驟4,將當前訂閱者中與當前訂閱者所訂閱的事件型別,新增到typesBySubscriber中去
        List<Class<?>> subscribedEvents = (List)this.typesBySubscriber.get(subscriber);
        if (subscribedEvents == null) {
            subscribedEvents = new ArrayList();
            this.typesBySubscriber.put(subscriber, subscribedEvents);
        }
        subscribedEvents.add(eventType);

        //步驟5,如果該方法有訂閱了粘性事件,則從stickyEvents中獲取相應粘性事件,併傳送
        if (subscriberMethod.sticky) {
            if (eventInheritance) {
                Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();
                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 = this.stickyEvents.get(eventType);
                this.checkPostStickyEventToSubscription(newSubscription, stickyEvent);
            }
        }

    }
複製程式碼

在上述方法中主要流程如下:

  • 步驟1,將每個訂閱方法和訂閱者封裝成 Subscription。
  • 步驟2,獲取對應事件中所有的 Subscription ,判斷是否重複新增。
  • 步驟3,根據 優先順序,將當前新封裝的 Subscription 物件新增到 subscriptionsByEventType 中去。(設定了優先順序後,EvenBus 就可以按照優先順序順序,將事件傳送給訂閱者)
  • 步驟4,將當前訂閱者中與當前訂閱者所訂閱的事件型別,新增到 typesBySubscriber 中去。
  • 步驟5,如果該方法有訂閱了粘性事件,則從 stickyEvents 中獲取相應粘性事件,併傳送。

再結合我們最開始所畫的 EventBus 大致流程,該方法其實就做了下圖紅色虛線框中的事:

subscribe()實際做的事.jpg

關於粘性事件的知識點,需要我們瞭解事件的傳送流程,我們會在下文進行詳細介紹。

事件傳送流程原始碼分析

事件的傳送,主要分為簡單事件粘性事件,分別對應方法為 post()postSticky() 兩個方法。這裡我們先看簡單事件的傳送,程式碼如下:

簡單事件的傳送

  public void post(Object event) {
     //步驟1,獲取當前執行緒中獨立擁有的PostingThreadState,並從中獲取事件佇列(eventQueue),將傳送的事件新增到該佇列中
        EventBus.PostingThreadState postingState = (EventBus.PostingThreadState)this.currentPostingThreadState.get();
        List<Object> eventQueue = postingState.eventQueue;
        eventQueue.add(event);

        //步驟2:判斷當前執行緒是否正在分發事件,如果不是,則迴圈遍歷事件佇列中的事件,並將事件分發出去,直到當前事件佇列空為止
        if (!postingState.isPosting) {
            postingState.isMainThread = this.isMainThread();
            postingState.isPosting = true;
            //如果當前分發事件狀態為取消,則丟擲異常
            if (postingState.canceled) {
                throw new EventBusException("Internal error. Abort state was not reset”);
            }
            //迴圈遍歷事件佇列,並將訊息傳送出去
            try {
                while(!eventQueue.isEmpty()) {
                    this.postSingleEvent(eventQueue.remove(0), postingState);
                }
            } finally {
                postingState.isPosting = false;
                postingState.isMainThread = false;
            }
        }

    }

複製程式碼

在 EventBus 中會為個每呼叫 post() 方法的執行緒都會建立一個唯一的 PostingThreadState 物件,用於記錄當前執行緒儲存傳送訊息與傳送的狀態,其內部結構如下所示:

PositingThreadState與執行緒的關係.jpg

PostingThreadState 使用了 ThreadLocal 不熟悉 ThreadLocal 的小夥伴,可以檢視該篇文章:Android Handler機制之ThreadLocal

也就是說當我們呼叫 EventBus.post() 方法,其實是從 EventQueue 佇列中取出訊息,然後通過呼叫 postSingleEvent()方法 來實際傳送訊息,該方法程式碼如下所示:

  private void postSingleEvent(Object event, EventBus.PostingThreadState postingState) throws Error {
        Class<?> eventClass = event.getClass();
        boolean subscriptionFound = false;
        //步驟1:?判斷否事件傳遞傳送
        if (this.eventInheritance) {
            List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
            int countTypes = eventTypes.size();
            for(int h = 0; h < countTypes; ++h) {
                Class<?> clazz = (Class)eventTypes.get(h);
                //?迴圈遍歷遍歷事件併傳送
                subscriptionFound |= this.postSingleEventForEventType(event, postingState, clazz);
            }
        } else {
            //步驟2:?如果不支援事件的傳遞,那麼這裡開始傳送事件。
            subscriptionFound = this.postSingleEventForEventType(event, postingState, eventClass);
        }
        //步驟3:如果沒有找到訂閱的方式,提示使用者
        if (!subscriptionFound) {
            if (this.logNoSubscriberMessages) {
                this.logger.log(Level.FINE, "No subscribers registered for event " + eventClass);
            }

            if (this.sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class && eventClass != SubscriberExceptionEvent.class) {
                this.post(new NoSubscriberEvent(this, event));
            }
        }

    }
複製程式碼

該方法主要為如下三個步驟:

  • 步驟1:通過布林變數 eventInheritance 判斷是否支援事件是否傳遞傳送,如果支援,那麼通過lookupAllEventTypes() 方法獲得傳送事件祖先類及其介面。然後通過 postSingleEventForEventType()方法,將它們都傳送出去,
  • 步驟2:步驟1返回 false 那麼就直接使用 postSingleEventForEventType() 方法傳送事件。
  • 步驟3:如果沒有找到相關的訂閱方法,那麼就提示使用者沒有相關的訂閱方法。

布林變數 eventInheritance 預設為 false ,我們可以通過 EventBusBuilder 來配置該變數的值。

那什麼是事件的傳遞傳送呢?我們來檢視 lookupAllEventTypes()方法:


    private static List<Class<?>> lookupAllEventTypes(Class<?> eventClass) {
        synchronized (eventTypesCache) {
            List<Class<?>> eventTypes = eventTypesCache.get(eventClass);
            if (eventTypes == null) {
                eventTypes = new ArrayList<>();
                Class<?> clazz = eventClass;
                //?獲取該類所有祖先類及其介面
                while (clazz != null) {
                    eventTypes.add(clazz);
                    addInterfaces(eventTypes, clazz.getInterfaces());
                    clazz = clazz.getSuperclass();
                }
                eventTypesCache.put(eventClass, eventTypes);
            }
            return eventTypes;
        }
    }

    //將介面新增到集合中
    static void addInterfaces(List<Class<?>> eventTypes, Class<?>[] interfaces) {
        for (Class<?> interfaceClass : interfaces) {
            if (!eventTypes.contains(interfaceClass)) {
                eventTypes.add(interfaceClass);
                addInterfaces(eventTypes, interfaceClass.getInterfaces());
            }
        }
    }
複製程式碼

在該方法中,會獲取傳送事件的所有的祖先類及其介面,最後將他們以集合的方式返回,在 postSingleEvent 方法中拿到這個集合之後,那麼就會將集合中所有的資料都傳送出去。這樣做會造成什麼效果呢?如果當前我們的繼承體系為 Aevent -> Bevent -> Cevent ( -> 表示繼承),那麼通過傳送 Aevent,那麼其他所有訂閱過 Bevent 及 Cevent 的訂閱者都會收到訊息。

我們繼續檢視 postSingleEventForEventType() 方法,程式碼如下所示:

    private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
        CopyOnWriteArrayList<Subscription> subscriptions;
        //?從快取中拿取之前存取的 Subscription
        synchronized (this) {
            subscriptions = subscriptionsByEventType.get(eventClass);
        }
        if (subscriptions != null && !subscriptions.isEmpty()) {
            for (Subscription subscription : subscriptions) {
                postingState.event = event;
                postingState.subscription = subscription;
                boolean aborted = false;
                try {
                    //?這裡找到相應的方法後,開始切換執行緒了。
                    postToSubscription(subscription, event, postingState.isMainThread);
                    aborted = postingState.canceled;
                } finally {
                    postingState.event = null;
                    postingState.subscription = null;
                    postingState.canceled = false;
                }
                if (aborted) {
                    break;
                }
            }
            return true;
        }
        return false;
    }

複製程式碼

該方法的邏輯非常簡單,就是從我們之前的 subscriptionsByEventType 集合中拿到儲存的 Subscription,並根據當前執行緒狀態設定關聯的 PostingStatecanceledsubscriptionisMainThread 等屬性值,然後通過 postToSubscription() 方法來真正的執行事件的傳遞。

到目前為止整個流程如下所示:

簡單事件的傳送.jpg

postToSubscription()

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 {
                    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);
        }
    }
複製程式碼

從上述方法中,我們拿到 Subscription 中成員變數 SubscriberMethod 中的執行緒模式 threadMode 來判斷訂閱方法需要執行的執行緒。如果當前執行緒模式是 POSTING ,那麼預設就直接呼叫 invokeSubscriber() 方法。具體程式碼如下所示:

    void invokeSubscriber(Subscription subscription, Object event) {
        try {
            //?直接通過反射呼叫訂閱方法。
            subscription.subscriberMethod.method.
            invoke(subscription.subscriber, event);
        }
        //省略部分程式碼
    }
複製程式碼

如果為其他模式,那麼會根據相應的 poster 呼叫 enqueue() 方法來控制執行訂閱方法所在的執行緒。在 EventBus 中提供瞭如下三個 Poster 來控制訂閱方法的所執行的執行緒。

  • HandlerPoster (切換到主執行緒)
  • BackgroundPoster (切換到後臺執行緒)
  • AsyncPoster (切換到後臺執行緒)

以上三個 Poster 都實現了 Poster 介面,且內部都維護了一個名為 PendingPostQueue 的佇列,該佇列以 PendingPost 為儲存單元,其中 PendingPost 中儲存內容為我們根據當前事件所找到的 Subscription 與當前所發生的事件。

那麼結合整個流程,我們能得到下圖:

簡單事件傳送的整個流程.jpg

針對上圖,再進行一下簡單的說明。

  • 當我們呼叫 EventBus.post() 傳送簡單事件時,會將該事件放入與執行緒相關的 PostingThreadStateEventQueue 中。
  • 接著會從之前在 subscriptionsByEventType 集合中找到與該事件相關的 Subscription
  • 接著將找到的 Subscription 與當前所傳送的事件都封裝為 PendingPost 並新增到對應 Poster 中的 PendingPostQueue 佇列中。
  • 最後對應的 Poster 從佇列中取出相應的 PendingPost,通過反射呼叫訂閱者的訂閱方法。

其中訂閱方法執行執行緒的規則,如下所示:

訂閱方法規則.png

執行緒的切換

在上節中,訂閱者的訂閱方法執行的所線上程,是由 EventBus 中內部的三個 Poster來實現的。那下面我們就來看看這三個 Poster 的實現。

HandlerPoster
public class HandlerPoster extends Handler implements Poster {

    private final PendingPostQueue queue;
    private final int maxMillisInsideHandleMessage;
    private final EventBus eventBus;
    private boolean handlerActive;

    //預設會傳遞主執行緒的Looper
    protected HandlerPoster(EventBus eventBus, Looper looper, int maxMillisInsideHandleMessage) {
        super(looper);
        this.eventBus = eventBus;
        this.maxMillisInsideHandleMessage = maxMillisInsideHandleMessage;
        queue = new PendingPostQueue();
    }

    public void enqueue(Subscription subscription, Object event) {
        PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
        synchronized (this) {
            //?這裡將PedingPost放入PendingPostQueue中,然後傳送訊息
            queue.enqueue(pendingPost);
            if (!handlerActive) {
                handlerActive = true;
                if (!sendMessage(obtainMessage())) {
                    throw new EventBusException("Could not send handler message”);
                }
            }
        }
    }

    @Override
    public void handleMessage(Message msg) {
        boolean rescheduled = false;
        try {
            long started = SystemClock.uptimeMillis();
            while (true) {
                //?從佇列中取出最近的PendingPost
                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;
        }
    }
}
複製程式碼

HanderPoster 中的邏輯非常容易理解,繼承 Handler,並在初始化的時候預設會關聯 主執行緒 的 Looper,這樣該 Handler 所傳送的訊息將會在主執行緒中被處理。

分析一下 HanderPoster 中主要的步驟:

  • 在呼叫 enqueue() 方法時,會將之前我們封裝好的 PendingPost 放入 PendingPostQueue 佇列中,同時傳送訊息。
  • handleMessage() 方法中,從 PendingPostQueue 佇列中取出最近的 PendingPost,然後直接通過 eventBus.invokeSubscriber() 反射執行訂閱者的訂閱方法。
BackgroundPoster
final class BackgroundPoster implements Runnable, Poster {

    private final PendingPostQueue queue;
    private final EventBus eventBus;

    private volatile boolean executorRunning;

    BackgroundPoster(EventBus eventBus) {
        this.eventBus = eventBus;
        queue = new PendingPostQueue();
    }

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

    @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) {
                eventBus.getLogger().log(Level.WARNING, Thread.currentThread().getName() + " was interruppted", e);
            }
        } finally {
            executorRunning = false;
        }
    }
}
複製程式碼

BackgroundPoster 與 HandlerPoster 最大的不同是其內部使用了執行緒池,並且該類也實現了 Runnable 介面。

在 BackgroundPoster 中的 enqueue() 方法中,預設會使用 EventBus 中預設的執行緒池 DEFAULT_EXECUTOR_SERVICE來提交任務 ,該執行緒池的宣告如下:

 private final static ExecutorService DEFAULT_EXECUTOR_SERVICE = Executors.newCachedThreadPool();
複製程式碼

CachedThreadPool 適用於大量的且耗時較少的任務

同樣的,BackgroundPoster 也就是通過反射呼叫訂閱者的訂閱方法,只不過不同的是它是放入執行緒池中的非主執行緒中進行執行。

需要注意的是不管是在任何執行緒中傳送訊息,EventBus 總是執行緒安全的。從 BackgroundPoster 的程式碼中我們就可以看出。

AsyncPoster
class AsyncPoster implements Runnable, Poster {

    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 進行講解了,相信大家根據之前的內容也能理解。

粘性事件的傳送

現在我們還剩最後一個知識點了,就是粘性事件的傳送。在 EventBus 中傳送粘性事件,我們需要呼叫方法 postSticky() 方法,程式碼如下所示:

  public void postSticky(Object event) {
        synchronized (stickyEvents) {
            stickyEvents.put(event.getClass(), event);
        }
        post(event);
    }
複製程式碼

從程式碼中,我們不難看出,粘性的事件傳送與簡單事件的傳送唯一的區別就是將傳送的事件新增到 stickyEvents 集合中去了。那為什麼要這麼做呢?在瞭解具體的原因之前,我們需要了解粘性事件的概念。

粘性事件的概念:當訂閱者還沒有訂閱相關事件 A 時,程式已經傳送了一些事件 A,按照正常的邏輯,當訂閱者開始訂閱事件 A 時,是接受不到程式已經傳送過的事件 A ,但是我們希望接受到那些已經傳送過的訊息。這種已經過時,但又被重新接受的事件,我們稱之為粘性事件。

那麼根據粘性事件的思想,我們需要將已經傳送的事件儲存下來,並在粘性事件的訂閱的過程中進行特別的處理,也就是在 EventBus.register() 方法中進行處理。還記得之前註冊過程中的 subscribe() 方法嗎?該方法內部對粘性事件進行了特殊的處理,程式碼如下所示:

 private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
        //省略部分程式碼
        //判斷是否是粘性事件
        if (subscriberMethod.sticky) {
            //?支援事件傳遞的粘性事件
            if (eventInheritance) {
                Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();
                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);
            }
        }
    }
複製程式碼

在上述邏輯中,會從 stickyEvents 中獲取之前傳送的事件,然後呼叫 checkPostStickyEventToSubscription()。該方法程式碼如下所示:

 private void checkPostStickyEventToSubscription(Subscription newSubscription, Object stickyEvent) {
        if (stickyEvent != null) {
            postToSubscription(newSubscription, stickyEvent, isMainThread());
        }
    }
複製程式碼

又因為checkPostStickyEventToSubscription() 方法內部會呼叫 postToSubscription() 方法。那麼最終訂閱者就能接受到之前傳送的事件,並執行相應的訂閱方法啦。

最後

EventBus 主要的流程到現在已經講完了。從實際的程式碼中,我們不僅能看到其良好的程式碼規範以及封裝思想。還能看到該框架對效能的優化,尤其是新增了一些必要的快取。我相信以上的這些點,都是值得我們借鑑與參考的。在接下來的文章中我們會講解 EventBus 中的 “加速引擎" 索引類。有興趣的小夥伴可以繼續關注。

相關文章