EventBus3.0原始碼解析

努力啊Ant發表於2019-04-15

EventBus 是一個用於元件間通訊的框架。它為開發提供一種非常簡便的方式來是實現元件間解耦通訊,並且提供了執行緒切換、優先順序設定等功能。

EventBus示意圖

從官方的示意圖中不難看出,EventBus使用的是觀察者模式Subscriber註冊到EventBus, 當Publisher使用post方法將Event傳送給EventBusEventBus就會回撥SubscriberonEvent方法。觀察者模式能將觀察者和訂閱者最大程度的解耦,這也是EventBus的功能所在。

具體用法就不多說了,具體可見官方主頁github.com/greenrobot/…

本文解析使用的EventBus版本是3.0.0

進行原始碼分析之前,先思考一下,如果是自己,會如何實現?

首先,我們需要將註冊的類、宣告的訂閱方法,以及方法執行的執行緒、優先順序都儲存下來; 其次,我們要可以根據接收的事件去快速查詢到對應的訂閱者,當有訊息通知時,可以高效通知; 再者,我們還需要自己去處理執行緒間的切換以滿足不同的應用場景; 最後,我們應該提供登出功能以便取消訊息訂閱。

帶著思路,一起來看看原始碼是如何一步一步實現的。本文根據我們使用EventBus的步驟來進行講解的。

註解@Subscribe

在宣告訂閱方法時,要求使用 @Subscribe註解,先來看下它的具體定義

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
    ThreadMode threadMode() default ThreadMode.POSTING;

    boolean sticky() default false;

    int priority() default 0;
}
複製程式碼

先來看三個元註解:

@Documented :生成java文件時,會將該註解也寫進文件裡

@Retention(RetentionPolicy.RUNTIME):有效週期是在執行時

@Target({ElementType.METHOD}):指定對方法有效。

註解裡宣告瞭三個成員變數:

  • threadMode(訂閱方法執行的執行緒)
  • sticky(是否是粘性事件)
  • priority(優先順序)。

ThreadMode是一個列舉類,定義了四種執行緒模式:

public enum ThreadMode {
    POSTING,
    MAIN,
    BACKGROUND,
    ASYNC
}
複製程式碼

POSTING: 和傳送事件的執行緒在同一個執行緒,避免了執行緒切換開銷。

MAIN:訂閱在主執行緒,事件將排隊等待交付(非阻塞)。使用此模式的訂閱者必須快速返回,以避免阻塞主執行緒。

BACKGROUND:如果是在主執行緒釋出,則會訂閱在一個後臺執行緒,依次排隊執行;如果不是在主執行緒釋出,則會訂閱在釋出所在的執行緒。

ASYNC: 在非主執行緒和釋出執行緒中訂閱。當處理事件的方法 是耗時的,需要使用此模式。儘量避免同時觸發大量的耗時較長的非同步操作,EventBus 使用執行緒池高效的複用已經完成非同步操作的執行緒。

EventBus 之所以要求在訂閱方法上加上@Subscribe註解,就是相當於給訂閱者打標籤,框架根據註解去找到訂閱者。

註冊

先來看看註冊的過程

public void register(Object subscriber) {

    //拿到訂閱者的執行時型別Class
    Class<?> subscriberClass = subscriber.getClass();
    
    //利用訂閱者的Class去查詢類中宣告的訂閱方法
    List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
        
    //迴圈遍歷逐個將訂閱者和訂閱方法訂閱到EventBus
    synchronized (this) {
        for (SubscriberMethod subscriberMethod : subscriberMethods) {
                subscribe(subscriber, subscriberMethod);
        }
    }
}
複製程式碼

主要有三步,第一步拿到訂閱者的類物件,第二步通過訂閱者類物件找到類中的所有訂閱方法,第三步進行訂閱。

查詢訂閱方法

在上述第二步使用了一個SubscriberMethodFinder例項來進行方法查詢。SubscriberMethodFinder這個類是專門用來查詢訂閱方法的,findSubscriberMethods()最後返回了一個SubscriberMethod集合。SubscriberMethod類則就是對我們宣告的訂閱方法和引數的封裝。可以先略過。

接著看findSubscriberMethods()是如何通過訂閱者的Class物件來進行方法查詢的。

   List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
   
        //從快取中查詢,如果已經有,則直接返回。
        List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
        if (subscriberMethods != null) {
            return subscriberMethods;
        }

        //如果快取中沒有查詢到,就通過訂閱者Class物件進行查詢。
        if (ignoreGeneratedIndex) {
            subscriberMethods = findUsingReflection(subscriberClass);
        } else {
            // 使用apt提前解析的訂閱者資訊
            subscriberMethods = 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;
        }
    }

複製程式碼

這裡做了個快取處理METHOD_CACHE,因為查詢是比較耗時的操作,快取可以提高效率。

private static final Map<Class<?>, List<SubscriberMethod>> METHOD_CACHE = new ConcurrentHashMap<>();
複製程式碼

METHOD_CACHE是一個以訂閱者Class為Key, 訂閱方法集合為Value的執行緒安全的的HashMap

第一次執行肯定是沒有快取的,然後會根據ignoreGeneratedIndex來執行不同的方法。從方法名來看,一個是使用反射去查詢,另一個是使用已有的資訊去查詢。

其實這裡就是3.0.0引入的優化點:3.0.0引入了APT(Annotation Processing Tool),它可以在編譯階段就提前解析註解,提前檢索到訂閱方法。這樣就提高了執行時效率。

ignoreGeneratedIndex這個值預設是false,因為反射開銷大,所以預設是走findUsingInfo()分支,但是在findUsingInfo()方法中會檢查本地是否有apt預先解析出的訂閱者資訊,如果沒有,還是會執行反射方法findUsingReflectionInSingleClass()

啟動apt的部分涉及到了註解處理器,所以會單獨起一篇來講解,先來看看不使用apt的情況:

    private List<SubscriberMethod> findUsingReflection(Class<?> subscriberClass) {
        //準備一個FindeState例項
        FindState findState = prepareFindState();
        //將訂閱者Class與FindeState例項關聯
        findState.initForSubscriber(subscriberClass);
        
        //從子類到父類去逐一查詢訂閱方法
        while (findState.clazz != null) {
           //使用反射查詢一個類的訂閱方法 findUsingReflectionInSingleClass(findState);
           //將父類賦給findState.clazz,往上進行查詢
            findState.moveToSuperclass();
        }
        //返回訂閱方法集合並回收FindState例項
        return getMethodsAndRelease(findState);
    }
複製程式碼

這裡去查詢訂閱方法,因為子類會繼承父類的方法,所以當子類找不到時,需要去查詢父類。這裡使用了一個while迴圈遞迴進行查詢。查詢過程使用了一個新的物件FindState,它是用來儲存查詢過程中的一些資訊,方便進行迭代查詢。它的類定義:

    static class FindState {
        //訂閱方法集合
        final List<SubscriberMethod> subscriberMethods = new ArrayList<>();
        final Map<Class, Object> anyMethodByEventType = new HashMap<>();
        final Map<String, Class> subscriberClassByMethodKey = new HashMap<>();
        final StringBuilder methodKeyBuilder = new StringBuilder(128);

        //當前訂閱者
        Class<?> subscriberClass;
        //當前查詢的類
        Class<?> clazz;
        //是否跳過父類查詢
        boolean skipSuperClasses;
        SubscriberInfo subscriberInfo;
    }
複製程式碼

接著往下走,進入真正開始使用反射解析一個類的訂閱方法:findUsingReflectionInSingleClass()

private void findUsingReflectionInSingleClass(FindState findState) {
        Method[] methods;
        try {
            // 只獲取非繼承的方法,這個方法比getMethods()方法效率高,比如對一個Activity來說。 findState.clazz.getDeclaredMethods();
        } catch (Throwable th) {
            // 異常則直接獲取所有方法(包括繼承的), 這樣就不用再檢查父類了。
            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();
                // 只能有一個引數
                if (parameterTypes.length == 1) {
                    //獲取@Subscribe註解的方法
                    Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
                    if (subscribeAnnotation != null) {
                        Class<?> eventType = parameterTypes[0];
                        //檢查該eventType是否已訂閱了,通常訂閱者不能有多個 eventType 相同的訂閱方法
                        if (findState.checkAdd(method, eventType)) {
                            ThreadMode threadMode = subscribeAnnotation.threadMode();
                            //將訂閱方法和對應的接收的Event型別以及註解引數儲存
                            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");
            }
        }
    }
複製程式碼

這個方法主要是通過修飾符、引數個數、指定註解和是否有EventType相同的方法這幾層篩選,最終將訂閱方法新增進 findStatesubscriberMethods 這個 List 中。

其中重點有一個判斷:findState.checkAdd(),這個方法決定了是否訂閱方法可以被儲存下來進而能接收到訊息。一起來看看它是如何判斷的:

private boolean checkAdd(Method method, Class<?> eventType) {
    // 1.檢查eventType是否已經註冊過對應的方法(一般都沒有) 
    Object existing = anyMethodByEventType.put(eventType, method);
    if (existing == null) {
        return true;
    } else {
        // 2. 如果已經有方法註冊了這個eventType
        if (existing instanceof Method) {
            if (!checkAddWithMethodSignature((Method) existing, eventType)) {
                throw new IllegalStateException();
            }
            anyMethodByEventType.put(eventType, this);
        }
        return checkAddWithMethodSignature(method, eventType);
    }
}
複製程式碼

進行了兩種檢查,第一種是判斷當前類中是否已經有這個EventType和對應的訂閱方法,一般一個類不會有對同一個EventType寫多個方法,會直接返回true,進行儲存。

但是如果出現了同一個類中同樣的EventType寫了多個方法,該如何處理?

還有當findUsingReflection()中進行下一輪迴圈,會進行父類查詢,如果子類繼承了父類的訂閱方法,又該如何處理呢?

答案就在上邊的註釋2。關鍵點就是checkAddWithMethodSignature()方法:

private boolean checkAddWithMethodSignature(Method method, Class<?> eventType) {
    // 以[方法名>eventType]為Key
    methodKeyBuilder.setLength(0);                                                   
    methodKeyBuilder.append(method.getName());                                       
    methodKeyBuilder.append('>').append(eventType.getName());
    String methodKey = methodKeyBuilder.toString();                  
    
    // 拿到新的訂閱方法所屬類
    Class<?> methodClass = method.getDeclaringClass();                               
    Class<?> methodClassOld = subscriberClassByMethodKey.put(methodKey, methodClass);
    
    if (methodClassOld == null || methodClassOld.isAssignableFrom(methodClass)) {    
        // methodClassOld == null或者 methodClassOld是methodClass的父類/同一個類     
        return true;                                                                 
    } else {                      
        // Revert the put, old class is further down the class hierarchy             
        subscriberClassByMethodKey.put(methodKey, methodClassOld);                   
        return false;                                                                
    }                                                                                
}
複製程式碼

對於同一類中同樣的EventType寫了多個方法,因為方法名不同,所以[方法名>eventType]Key不同,methodClassOld會為null,直接返回 true。所以這種情況會將所有相同EventType的方法都進行儲存。

對於子類重寫父類方法的情況,則methodClassOld(即子類)不為null,並且methodClassOld也不是methodClass的父類,所以會返回false。即對於子類重寫父類訂閱方法,只會儲存子類的訂閱方法,忽略父類的訂閱方法。

至此,findState的查詢任務就結束了,通過迴圈向父類查詢,將訂閱者的訂閱方法都儲存在了其內部變數subscriberMethods列表中。

最後,跳出迴圈,回到findUsingReflection()方法中,最後返回了 getMethodsAndRelease(findState)

    private List<SubscriberMethod> getMethodsAndRelease(FindState findState) {
        List<SubscriberMethod> subscriberMethods = new ArrayList<>(findState.subscriberMethods);
        findState.recycle();
        synchronized (FIND_STATE_POOL) {
            for (int i = 0; i < POOL_SIZE; i++) {
                if (FIND_STATE_POOL[i] == null) {
                    FIND_STATE_POOL[i] = findState;
                    break;
                }
            }
        }
        return subscriberMethods;
    }
複製程式碼

很好理解,就是將findState中的subscriberMethods取出並返回。可以看到作者還是很細心的,將使用完的findState例項置空恢復後又放回例項池中,將例項回收利用,節省了新的開銷。

再回到findSubscriberMethods()方法中,將查詢的方法最後都存進了記憶體快取METHOD_CACHE中, 對應關係是訂閱類和它的訂閱方法:

METHOD_CACHE.put(subscriberClass, subscriberMethods);
複製程式碼

到這裡,查詢訂閱方法就結束了。

訂閱(subscribe())

回到register()方法中的第三步,對上一步查詢到的訂閱方法集合進行了遍歷呼叫subscribe()方法。來看看subscribe()方法做了什麼事:

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
        Class<?> eventType = subscriberMethod.eventType;
        // 例項一個Subscription物件,內部持有了訂閱者和訂閱方法
        Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
        // subscriptionsByEventType是以EventType為Key,以它對應Subscription集合的Map
        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);
            }
        }

        // 根據優先順序排序Subscription
        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;
            }
        }

        // typesBySubscriber儲存了訂閱者對應的所有EventType
        List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
        if (subscribedEvents == null) {
            subscribedEvents = new ArrayList<>();
            typesBySubscriber.put(subscriber, subscribedEvents);
        }
        subscribedEvents.add(eventType);

        // 粘性訂閱方法要立即處理
        if (subscriberMethod.sticky) {
            // 預設為true
            if (eventInheritance) {
                // 看當前EventType是否是已有的stickyEvent的子類或父類
                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);
            }
        }
    }
複製程式碼

這裡其實只做了三件事:

  1. 將訂閱者和訂閱方法封裝到subscriptionsByEventType,它可以根據EventType拿到所有的Subscription物件,Subscription物件中就有訂閱者和訂閱方法。這樣當有EventType訊息過來時,可以快速的傳遞給訂閱者的訂閱方法。
  2. 將訂閱者和訂閱方法封裝到typesBySubscriber,它可以根據訂閱類拿到所有的EventType。這樣當我們呼叫呼叫 unregister(this) 時,就可以拿到EventType,又根據EventType拿到所有訂閱者和方法,進行解綁了。
  3. 如果當前訂閱方法是粘性方法,則立即去查詢是否有本地事件,有的話就立即投遞。

至此,我們的註冊就完成了。可以看到某些方法棧的呼叫還是非常深的,但是整體流程卻很簡單。這也是值得我們學習的地方。

登出

註冊對應的就是登出,當我們的訂閱者不再需要接收事件時,可以呼叫unregister進行登出:

public synchronized void unregister(Object subscriber) {
        // 通過訂閱者拿到它訂閱的所有的訂閱事件型別
        List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
        if (subscribedTypes != null) {
            // 遍歷事件型別集合,根據事件型別解綁
            for (Class<?> eventType : subscribedTypes) {
                unsubscribeByEventType(subscriber, eventType);
            }
            // 從記錄中移除訂閱者
            typesBySubscriber.remove(subscriber);
        } else {
            Log.w(TAG, "Subscriber to unregister was not registered before: " + subscriber.getClass());
        }
    }
複製程式碼

程式碼很簡單,繼續看unsubscribeByEventType

private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {
        // 根據事件型別拿到所有的Subscription
        List<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        if (subscriptions != null) {
            // 遍歷所有Subscription,符合解除條件的進行remove
            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--;
                }
            }
        }
    }
複製程式碼

相比註冊流程,登出流程就非常簡單了,就是把對應的註冊者和對應的註冊資訊從記錄中移除即可。

傳送事件

註冊完畢後,我們的訂閱者和訂閱方法都被記錄在了EventBus裡,這時就可以給訂閱者們傳送事件了。EventBus提供了兩種傳送方法post()postSticky()post()傳送的是非粘性的事件,postSticky()傳送的是粘性事件。

post

先來看看post()傳送:

public void post(Object event) {
        // 從當前執行緒中取出PostingThreadState
        PostingThreadState postingState = currentPostingThreadState.get();
        // 拿到EventType佇列
        List<Object> eventQueue = postingState.eventQueue;
        eventQueue.add(event);

        // 當前執行緒是否有訊息正在投遞
        if (!postingState.isPosting) {
            postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();
            //正在投遞
            postingState.isPosting = true;
            if (postingState.canceled) {
                throw new EventBusException("Internal error. Abort state was not reset");
            }
            try {
                // 迴圈一個個進行投遞
                while (!eventQueue.isEmpty()) {
                    // 投遞出去就remove掉
                    postSingleEvent(eventQueue.remove(0), postingState);
                }
            } finally {
                // 所有訊息都投遞完成
                postingState.isPosting = false;
                postingState.isMainThread = false;
            }
        }
    }

複製程式碼

第一步使用ThreadLocal,是因為ThreadLocal保證了資料只對當前執行緒可見,其他執行緒是不可見的,這樣的話當我們從不同的執行緒中去取資料,資料相當於是分開儲存,設定和讀取就會比較快。從當前執行緒中取出的是一個PostingThreadState:

    final static class PostingThreadState {
        final List<Object> eventQueue = new ArrayList<Object>();
        boolean isPosting;
        boolean isMainThread;
        Subscription subscription;
        Object event;
        boolean canceled;
    }
複製程式碼

PostingThreadState 主要包含了當前執行緒的Event佇列、訂閱者資訊、事件等資料。接下來就是迴圈呼叫postSingleEvent()方法進行投送了:

private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
        Class<?> eventClass = event.getClass();
        boolean subscriptionFound = false;
        // 如果是可繼承的事件
        if (eventInheritance) {
            // 查詢Event的所有父類、介面類以及父類的介面類(Event的父類和介面也是Event)
            List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
            int countTypes = eventTypes.size();
            for (int h = 0; h < countTypes; h++) {
                Class<?> clazz = eventTypes.get(h);
                // 根據查詢到的所有Class(Event),逐個尋找訂閱者,進行分發event
                subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
            }
        } else {
            // 如果不是可繼承的事件,則直接對
            subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
        }
        // 如果沒有找到訂閱者,報異常
        if (!subscriptionFound) {
            if (logNoSubscriberMessages) {
                Log.d(TAG, "No subscribers registered for event " + eventClass);
            }
            if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class &&
                    eventClass != SubscriberExceptionEvent.class) {
                post(new NoSubscriberEvent(this, event));
            }
        }
    }
複製程式碼

這個方法的作用很簡單,就是做了個分支處理:如果是可繼承的事件,則查詢到它的所有父事件,然後再往下分發。繼續看postSingleEventForEventType()方法:

private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
        CopyOnWriteArrayList<Subscription> subscriptions;
        // 可能同時在多個執行緒同時傳送Event,subscriptionsByEventType是共有常量,所以需要加鎖
        synchronized (this) {
            // 根據Event的型別拿到所有訂閱者
            subscriptions = subscriptionsByEventType.get(eventClass);
        }
        if (subscriptions != null && !subscriptions.isEmpty()) {
            for (Subscription subscription : subscriptions) {
                // 事件還是Event,但是可能會分發到訂閱它父類的訂閱者中
                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;
    }
複製程式碼

這個方法和上一個方法的職責也很單一,就是查詢所有訂閱者,然後遍歷進行通知。查詢用的是一個Map: postToSubscription,就是在訂閱時生成的一個[EventType -> List<Subscription>] Map, 這樣我們就可以根據EventType查詢到所有訂閱者。接著看postToSubscription()

private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
        // 根據訂閱執行緒模式進行switch
        switch (subscription.subscriberMethod.threadMode) {
            case POSTING:
                // 直接呼叫在本執行緒
                invokeSubscriber(subscription, event);
                break;
            case MAIN:
                if (isMainThread) {
                    // 如果就在主執行緒,則直接呼叫
                    invokeSubscriber(subscription, event);
                } else {
                    // 如果不在主執行緒,則使用mainThreadPoster
                    mainThreadPoster.enqueue(subscription, event);
                }
                break;
            case BACKGROUND:
                if (isMainThread) {
                    // 如果在主執行緒,使用backgroundPoster
                    backgroundPoster.enqueue(subscription, event);
                } else {
                    // 如果不在主執行緒,則直接在當前執行緒呼叫
                    invokeSubscriber(subscription, event);
                }
                break;
            case ASYNC:
                // 啟動新的執行緒呼叫,asyncPoster
                asyncPoster.enqueue(subscription, event);
                break;
            default:
                throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
        }
    }
複製程式碼

這裡就是EventBus的執行緒切換了,主要有POSTINGMAINBACKGROUNDASYNC四種模式,四種模式在開篇已經介紹過了。這裡涉及到了invokeSubscriber()方法和mainThreadPosterbackgroundPosterasyncPoster三個poster,接下來我們就分別來看下:

invokeSubscriber()

    void invokeSubscriber(Subscription subscription, Object event) {
        try {
            subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
        } catch (InvocationTargetException e) {
            handleSubscriberException(subscription, event, e.getCause());
        } catch (IllegalAccessException e) {
            throw new IllegalStateException("Unexpected exception", e);
        }
    }
複製程式碼

invokeSubscriber()就是直接在當前執行緒呼叫了訂閱者的method物件,這裡呼叫了反射類Method的方法invoke()直接呼叫執行。

mainThreadPoster

mainThreadPoster = new HandlerPoster(this, Looper.getMainLooper(), 10);
複製程式碼

mainThreadPoster是個自定義的類HandlerPoster,它的目的是在主執行緒中呼叫訂閱方法,而EventBus使用的就是我們熟悉的Handler

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.getMainLooper()即主執行緒的Handler。 內部定義了一個最大處理訊息時間,預設是10毫秒,所以說我們一定不要在訂閱方法中做耗時操作。還維護了一個PendingPostQueue,它是自定義的一個連結串列佇列,這裡猜測HandlerPoster可能是自己維護了訊息佇列,來看下入隊方法:

    void enqueue(Subscription subscription, Object event) {
        // 獲取一個PendingPost例項
        PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
        synchronized (this) {
            // 入隊
            queue.enqueue(pendingPost);
            if (!handlerActive) {
                handlerActive = true;
                // 主執行緒的handler傳送訊息,傳送到主執行緒
                if (!sendMessage(obtainMessage())) {
                    throw new EventBusException("Could not send handler message");
                }
            }
        }
    }
複製程式碼

這裡出現了一個新的類PendingPost,它封裝了訂閱者subscription例項和事件event例項;它內部又維護了一個大小為10000PendingPost(陣列集合)池,用來重複利用PendingPost例項。然後將PendingPost例項入隊到上一步說的PendingPostQueue佇列中。接著使用主執行緒的Handler傳送一個訊息。接下來就是在handleMessage()中如何處理訊息了:

@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;
                        }
                    }
                }
                // 呼叫訂閱方法並會回收pendingPost
                eventBus.invokeSubscriber(pendingPost);
                long timeInMethod = SystemClock.uptimeMillis() - started;
                // 如果方法的執行時間超過最大執行時間(預設10毫秒)
                if (timeInMethod >= maxMillisInsideHandleMessage) {
                    if (!sendMessage(obtainMessage())) {
                        throw new EventBusException("Could not send handler message");
                    }
                    rescheduled = true;
                    return;
                }
            }
        } finally {
            handlerActive = rescheduled;
        }
    }
複製程式碼

這裡就是使用了while迴圈,不斷從佇列中去取PendingPost處理,但是加了個最大執行時間處理,因為是在主執行緒呼叫,所以一旦超時,就退出佇列,並重新嘗試去再進入佇列。

BackgroundPoster

再來看看BackgroundPoster

BackgroundPoster的作用是將UI執行緒的訂閱方法排程在非UI執行緒中。即它是要執行在新的Thread中的,而開啟執行緒我們最常用的就是Runnable, 來看看原始碼:

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介面,這樣就可以被執行緒執行。其內部也是維護了EventBus和一個PendingPost佇列。

    public void enqueue(Subscription subscription, Object event) {
        // 從訊息池中構建一個PendingPost
        PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
        synchronized (this) {
            // 入隊
            queue.enqueue(pendingPost);
            if (!executorRunning) {
                executorRunning = true;
                // 執行緒池排程執行
                eventBus.getExecutorService().execute(this);
            }
        }
    }
複製程式碼

HandlerPoster類似:新建一個新的PendingPost入隊,使用了EventBus裡的一個ExecutorService,它是對執行緒池定義的一個介面,來看看它的預設值:

public class EventBusBuilder {
    private final static ExecutorService DEFAULT_EXECUTOR_SERVICE = Executors.newCachedThreadPool();
    
    ...
    ExecutorService executorService = DEFAULT_EXECUTOR_SERVICE;
}
複製程式碼

熟悉的Executors,建立了一個可快取的執行緒池,用來執行BackgroundPoster這個Runnable 物件,再來看看BackgroundPosterrun()方法:

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

也和HandlerPoster類似,但是不同之處在於:

  1. 使用了poll(int maxMillisToWait)方法,這個設計很巧妙,當取到最後發現佇列為空後,會wait 1000 毫秒,當有有新的資訊來臨時就會喚醒執行緒,poll出訊息。這樣設計就減少了傳送訊息的次數,節省了資源。
  2. 因為是在子執行緒執行,所以就沒有方法執行時間的限制了。

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

}
複製程式碼

可以看到AsyncPosterBackgroundPoster``非常的相似,因為它們的功能也非常相似。但是不同之處在於:BackgroundPoster是儘可能使用一個後臺線層去依次排隊執行訂閱方法;而AsyncPoster則是每條訊息都直接開啟新的後臺執行緒立即執行。

至此四個Poster就講完了,看完是真的爽,不論是從功能抽象到具體細節的把控,EventBus都處理的很好,非常值得學習。

粘性事件

粘性事件這名字一聽很耳熟,沒錯,安卓四大元件之BroadcastReceiver就有一種廣播叫做粘性廣播(StickyBroadcast),而EventBus也提供了類似的功能:當註冊了粘性事件後,立即能收到還沒有註冊時系統發出的最後一個事件。

    public void postSticky(Object event) {
        // 將粘性事件儲存下來
        synchronized (stickyEvents) {
            stickyEvents.put(event.getClass(), event);
        }
        // Should be posted after it is putted, in case the subscriber wants to remove immediately
        post(event);
    }

複製程式碼

postSticky()方法用來傳送一個粘性事件,在這個方法中,直接將粘性事件儲存在了一個Map集合中,而key就是Event的Class物件。接著就呼叫正常的post()方法了。

那為什麼我們後註冊的方法也能接收到之前發出的粘性事件呢,答案就在上面提到的註冊方法subscribe()中的最後一段:

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
    ...
    
    // 如果是粘性事件,則直接傳送出去
    if (subscriberMethod.sticky) {
            if (eventInheritance) {
                // 從stickyEvents取出粘性事件的Class物件
                Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();
                for (Map.Entry<Class<?>, Object> entry : entries) {
                    Class<?> candidateEventType = entry.getKey();
                    // 如果訂閱的事件是儲存的粘性事件Class或它的父類
                    if (eventType.isAssignableFrom(candidateEventType)) {
                        // 取出快取的Event
                        Object stickyEvent = entry.getValue();
                        // 將快取的Event傳送出去
                        checkPostStickyEventToSubscription(newSubscription, stickyEvent);
                    }
                }
            } else {
                Object stickyEvent = stickyEvents.get(eventType);
                checkPostStickyEventToSubscription(newSubscription, stickyEvent);
            }
        }
}
複製程式碼

在我們註冊訂閱方法和事件時,如果是粘性事件,就直接會將事件傳送給註冊了相同Event的訂閱者,方法中呼叫了checkPostStickyEventToSubscription(newSubscription, stickyEvent)方法:

    private void checkPostStickyEventToSubscription(Subscription newSubscription, Object stickyEvent) {
        if (stickyEvent != null) {
            // If the subscriber is trying to abort the event, it will fail (event is not tracked in posting state)
            // --> Strange corner case, which we don't take care of here.
            postToSubscription(newSubscription, stickyEvent, Looper.getMainLooper() == Looper.myLooper());
        }
    }
複製程式碼

很簡單,直接又呼叫了postToSubscription()方法,根據指定執行緒分別進行分發。

總結

整體看下來,框架的結構非常的清晰全面,尤其在對方法功能的封裝和細節處理上,很是值得學習。尤其在3.0後,EventBus使用了註解處理器在編譯階段就完成了訂閱者的解析,這使得框架更佳的輕量和高效。整片原始碼讀下來還是很爽的,沒事還要再複習多看幾遍,很多東西都值得學習。共勉!

相關文章