Guava 原始碼分析之 EventBus 原始碼分析

WngShhng發表於2018-08-01

EventBus的設計理念是基於觀察者模式的,可以參考設計模式(1)—觀察者模式先來了解該設計模式。

1、程式示例

EventBus的使用是非常簡單的,首先你要新增Guava的依賴到自己的專案中。這裡我們通過一個最基本的例子來說明EveentBus是如何使用的。

public static void main(String...args) {
    // 定義一個EventBus物件,這裡的Joker是該物件的id
    EventBus eventBus = new EventBus("Joker");
    // 向上述EventBus物件中註冊一個監聽物件	
    eventBus.register(new EventListener());
    // 使用EventBus釋出一個事件,該事件會給通知到所有註冊的監聽者
    eventBus.post(new Event("Hello every listener, joke begins..."));
}

// 事件,監聽者監聽的事件的包裝物件
public static class Event {
    public String message;
    Event(String message) {
        this.message = message;
    }
}

// 監聽者
public static class EventListener {
    // 監聽的方法,必須使用註解宣告,且只能有一個引數,實際觸發一個事件的時候會根據引數型別觸發方法
    @Subscribe
    public void listen(Event event) {
        System.out.println("Event listener 1 event.message = " + event.message);
    }
}
複製程式碼

首先,這裡我們封裝了一個事件物件Event,一個監聽者物件EventListener。然後,我們用EventBus的構造方法建立了一個EventBus例項,並將上述監聽者例項註冊進去。然後,我們使用上述EventBus例項釋出一個事件Event。然後,以上註冊的監聽者中的使用@Subscribe註解宣告並且只有一個Event型別的引數的方法將會在觸發事件的時候被觸發。

總結:從上面的使用中,我們可以看出,EventBus與觀察者模式不同的地方在於:當註冊了一個監聽者的時候,只有當某個方法使用了@Subscribe註解宣告並且引數與釋出的事件型別匹配,那麼這個方法才會被觸發。這就是說,同一個監聽者可以監聽多種型別的事件,也可以在多次監聽同一個事件。

2、EventBus原始碼分析

2.1 分析之前

好了,通過上面的例子,我們瞭解了EventBus最基本的使用方法。下面我們來分析一下在Guava中是如何為我們實現這個API的。不過,首先,我們還是先試著考慮一下自己設計這個API的時候如何設計,並且提出幾個問題,然後帶著問題到原始碼中尋找答案。

假如要我們去設計這樣一個API,最簡單的方式就是在觀察者模式上進行擴充:每次呼叫EventBus.post()方法的時候,會對所有的觀察者物件進行遍歷,然後獲取它們全部的方法,判斷該方法是否使用了@Subscribe並且方法的引數型別是否與post()方法釋出的事件型別一致,如果一致的話,那麼我們就使用反射來觸發這個方法。在觀察者模式中,每個觀察者都要實現一個介面,釋出事件的時候,我們只要呼叫介面的方法就行,但是EventBus把這個限制設定得更加寬泛,也就是監聽者無需實現任何介面,只要方法使用了註解並且引數匹配即可。

從上面的分析中可以看出,這裡面不僅要對所有的監聽者進行遍歷,還要對它們的方法進行遍歷,找到了匹配的方法之後又要使用反射來觸發這個方法。首先,當註冊的監聽者數量比較多的時候,鏈式呼叫的效率就不高;然後我們又要使用反射來觸發匹配的方法,這樣效率肯定又低了一些。那麼在GuavaEventBus中是如何解決這兩個問題的?

另外還要注意下下文中的觀察者監聽者的不同,監聽者用來指我們使用EventBus.register()註冊的物件,觀察者是EventBus中的物件Subscriber,後者封裝了一個監聽者的所有的資訊,比如監聽的方法等等。 一般我們是不會直接操作Subscriber物件的,它的訪問許可權也只在EventBus的包中可訪問。

2.2 著手分析

首先,當我們使用new初始化一個EventBus的時候,實際都會呼叫到下面的這個方法:

EventBus(String identifier, Executor executor, Dispatcher dispatcher, SubscriberExceptionHandler exceptionHandler) {
    this.subscribers = new SubscriberRegistry(this);
    this.identifier = (String)Preconditions.checkNotNull(identifier);
    this.executor = (Executor)Preconditions.checkNotNull(executor);
    this.dispatcher = (Dispatcher)Preconditions.checkNotNull(dispatcher);
    this.exceptionHandler = (SubscriberExceptionHandler)Preconditions.checkNotNull(exceptionHandler);
}
複製程式碼

這裡的identifier是一個字串型別,類似於EventBus的id; subscribers是SubscriberRegistry型別的,實際上EventBus在新增、移除和遍歷觀察者的時候都會使用該例項的方法,所有的觀察者資訊也都維護在該例項中; executor是事件分發過程中使用到的執行緒池,可以自己實現; dispatcher是Dispatcher型別的子類,用來在釋出事件的時候分發訊息給監聽者,它有幾個預設的實現,分別針對不同的分發方式; exceptionHandler是SubscriberExceptionHandler型別的,它用來處理異常資訊,在預設的EventBus實現中,會在出現異常的時候列印出log,當然我們也可以定義自己的異常處理策咯。

所以,從上面的分析中可以看出,如果我們想要了解EventBus是如何註冊和取消註冊以及如何遍歷來觸發事件的,就應該從SubscriberRegistry入手。確實,個人也認為,這個類的實現也是EventBus中最精彩的部分。

2.2.1 SubscriberRegistry

根據2.1中的分析,我們需要在EventBus中維護幾個對映,以便在釋出事件的時候找到並通知所有的監聽者,首先是事件型別->觀察者列表的對映。 上面我們也說過,EventBus中釋出事件是針對各個方法的,我們將一個事件對應的型別資訊和方法資訊等都維護在一個物件中,在EventBus中就是觀察者Subscriber。 然後,通過事件型別對映到觀察者列表,當釋出事件的時候,只要根據事件型別到列表中尋找所有的觀察者並觸發監聽方法即可。 在SubscriberRegistry中通過如下資料結構來完成這一對映:

private final ConcurrentMap<Class<?>, CopyOnWriteArraySet<Subscriber>> subscribers = Maps.newConcurrentMap();
複製程式碼

從上面的定義形式中我們可以看出,這裡使用的是事件的Class型別對映到Subscriber列表的。這裡的Subscriber列表使用的是Java中的CopyOnWriteArraySet集合, 它底層使用了CopyOnWriteArrayList,並對其進行了封裝,也就是在基本的集合上面增加了去重的操作。這是一種適用於讀多寫少場景的集合,在讀取資料的時候不會加鎖, 寫入資料的時候進行加鎖,並且會進行一次陣列拷貝。

既然,我們已經知道了在SubscriberRegistry內部會在註冊的時候向以上資料結構中插入對映,那麼我們可以具體看下它是如何完成這一操作的。

在分析register()方法之前,我們先看下SubscriberRegistry內部經常使用的幾個方法,它們的原理與我們上面提出的問題息息相關。 首先是findAllSubscribers()方法,它用來獲取指定監聽者對應的全部觀察者集合。下面是它的程式碼:

private Multimap<Class<?>, Subscriber> findAllSubscribers(Object listener) {
    // 建立一個雜湊表
    Multimap<Class<?>, Subscriber> methodsInListener = HashMultimap.create();
    // 獲取監聽者的型別
    Class<?> clazz = listener.getClass();
    // 獲取上述監聽者的全部監聽方法
    UnmodifiableIterator var4 = getAnnotatedMethods(clazz).iterator(); // 1
    // 遍歷上述方法,並且根據方法和型別引數建立觀察者並將其插入到對映表中
    while(var4.hasNext()) {
        Method method = (Method)var4.next();
        Class<?>[] parameterTypes = method.getParameterTypes();
        // 事件型別
        Class<?> eventType = parameterTypes[0];
        methodsInListener.put(eventType, Subscriber.create(this.bus, listener, method));
    }
    return methodsInListener;
}
複製程式碼

這裡注意一下Multimap資料結構,它是Guava中提供的集合結構,與普通的雜湊表不同的地方在於,它可以完成一對多操作。這裡用來儲存事件型別到觀察者的一對多對映。 注意下1處的程式碼,我們上面也提到過,當新註冊監聽者的時候,用反射獲取全部方法並進行判斷的過程非常浪費效能,而這裡就是這個問題的答案:

這裡getAnnotatedMethods()方法會嘗試從subscriberMethodsCache中獲取所有的註冊監聽的方法(即使用了註解並且只有一個引數),下面是這個方法的定義:

private static ImmutableList<Method> getAnnotatedMethods(Class<?> clazz) {
    return (ImmutableList)subscriberMethodsCache.getUnchecked(clazz);
}
複製程式碼

這裡的subscriberMethodsCache的定義是:

private static final LoadingCache<Class<?>, ImmutableList<Method>> subscriberMethodsCache = CacheBuilder.newBuilder().weakKeys().build(new CacheLoader<Class<?>, ImmutableList<Method>>() {
    public ImmutableList<Method> load(Class<?> concreteClass) throws Exception { // 2
        return SubscriberRegistry.getAnnotatedMethodsNotCached(concreteClass);
    }
});
複製程式碼

這裡的作用機制是:當使用subscriberMethodsCache.getUnchecked(clazz)獲取指定監聽者中的方法的時候會先嚐試從快取中進行獲取,如果快取中不存在就會執行2處的程式碼, 呼叫SubscriberRegistry中的getAnnotatedMethodsNotCached()方法獲取這些監聽方法。這裡我們省去該方法的定義,具體可以看下原始碼中的定於,其實就是使用反射並完成一些校驗,並不複雜。

這樣,我們就分析完了findAllSubscribers()方法,整理一下:當註冊監聽者的時候,首先會拿到該監聽者的型別,然後從快取中嘗試獲取該監聽者對應的所有監聽方法,如果沒有的話就遍歷該類的方法進行獲取,並新增到快取中; 然後,會遍歷上述拿到的方法集合,根據事件的型別(從方法引數得知)和監聽者等資訊建立一個觀察者,並將事件型別-觀察者鍵值對插入到一個一對多對映表中並返回。

下面,我們看下EventBus中的register()方法的程式碼:

void register(Object listener) {
    // 獲取事件型別-觀察者對映表
    Multimap<Class<?>, Subscriber> listenerMethods = this.findAllSubscribers(listener);
    Collection eventMethodsInListener;
    CopyOnWriteArraySet eventSubscribers;
    // 遍歷上述對映表並將新註冊的觀察者對映表新增到全域性的subscribers中
	for(Iterator var3 = listenerMethods.asMap().entrySet().iterator(); var3.hasNext(); eventSubscribers.addAll(eventMethodsInListener)) {
        Entry<Class<?>, Collection<Subscriber>> entry = (Entry)var3.next();
        Class<?> eventType = (Class)entry.getKey();
        eventMethodsInListener = (Collection)entry.getValue();
        eventSubscribers = (CopyOnWriteArraySet)this.subscribers.get(eventType);
        // 如果指定事件對應的觀察者列表不存在就建立一個新的
        if (eventSubscribers == null) {
            CopyOnWriteArraySet<Subscriber> newSet = new CopyOnWriteArraySet();
            eventSubscribers = (CopyOnWriteArraySet)MoreObjects.firstNonNull(this.subscribers.putIfAbsent(eventType, newSet), newSet);
        }
    }
}
複製程式碼

SubscriberRegistry中的register()方法與unregister()方法類似,我們不進行說明。下面看下當呼叫EventBus.post()方法的時候的邏輯。下面是其程式碼:

public void post(Object event) {
    // 呼叫SubscriberRegistry的getSubscribers方法獲取該事件對應的全部觀察者
    Iterator<Subscriber> eventSubscribers = this.subscribers.getSubscribers(event);
    if (eventSubscribers.hasNext()) {
        // 使用Dispatcher對事件進行分發
        this.dispatcher.dispatch(event, eventSubscribers);
    } else if (!(event instanceof DeadEvent)) {
        this.post(new DeadEvent(this, event));
    }
}
複製程式碼

從上面的程式碼可以看出,實際上當呼叫EventBus.post()方法的時候回先用SubscriberRegistry的getSubscribers方法獲取該事件對應的全部觀察者,所以我們需要先看下這個邏輯。 以下是該方法的定義:

Iterator<Subscriber> getSubscribers(Object event) {
    // 獲取事件型別的所有父型別和自身構成的集合
    ImmutableSet<Class<?>> eventTypes = flattenHierarchy(event.getClass()); // 3
    List<Iterator<Subscriber>> subscriberIterators = Lists.newArrayListWithCapacity(eventTypes.size());
    UnmodifiableIterator var4 = eventTypes.iterator();
    // 遍歷上述事件型別,並從subscribers中獲取所有的觀察者列表
    while(var4.hasNext()) {
        Class<?> eventType = (Class)var4.next();
        CopyOnWriteArraySet<Subscriber> eventSubscribers = (CopyOnWriteArraySet)this.subscribers.get(eventType);
        if (eventSubscribers != null) {
            subscriberIterators.add(eventSubscribers.iterator());
        }
    }
    return Iterators.concat(subscriberIterators.iterator());
}
複製程式碼

這裡注意以下3處的程式碼,它用來獲取當前事件的所有的父類包含自身的型別構成的集合,也就是說,加入我們觸發了一個Interger型別的事件,那麼Number和Object等型別的監聽方法都能接收到這個事件並觸發。這裡的邏輯很簡單,就是根據事件的型別,找到它及其所有的父類的型別對應的觀察者並返回。

2.2.2 Dispatcher

接下來我們看真正的分發事件的邏輯是什麼樣的。

EventBus.post()方法可以看出,當我們使用Dispatcher進行事件分發的時候,需要將當前的事件和所有的觀察者作為引數傳入到方法中。然後,在方法的內部進行分發操作。最終某個監聽者的監聽方法是使用反射進行觸發的,這部分邏輯在Subscriber內部,而Dispatcher是事件分發的方式的策略介面。EventBus中提供了3個預設的Dispatcher實現,分別用於不同場景的事件分發:

  1. ImmediateDispatcher:直接在當前執行緒中遍歷所有的觀察者並進行事件分發;
  2. LegacyAsyncDispatcher:非同步方法,存在兩個迴圈,一先一後,前者用於不斷往全域性的佇列中塞入封裝的觀察者物件,後者用於不斷從佇列中取出觀察者物件進行事件分發;實際上,EventBus有個字類AsyncEventBus就是用該分發器進行事件分發的。
  3. PerThreadQueuedDispatcher:這種分發器使用了兩個執行緒區域性變數進行控制,當dispatch()方法被呼叫的時候,會先獲取當前執行緒的觀察者佇列,並將傳入的觀察者列表傳入到該佇列中;然後通過一個布林型別的執行緒區域性變數,判斷當前執行緒是否正在進行分發操作,如果沒有在進行分發操作,就通過遍歷上述佇列進行事件分發。

上述三個分發器內部最終都會呼叫Subscriber的dispatchEvent()方法進行事件分發:

final void dispatchEvent(final Object event) {
    // 使用指定的執行器執行任務
    this.executor.execute(new Runnable() {
        public void run() {
            try {
                // 使用反射觸發監聽方法
                Subscriber.this.invokeSubscriberMethod(event);
            } catch (InvocationTargetException var2) {
                // 使用EventBus內部的SubscriberExceptionHandler處理異常
                Subscriber.this.bus.handleSubscriberException(var2.getCause(), Subscriber.this.context(event));
            }
        }
    });
}
複製程式碼

上述方法中的executor是執行器,它是通過EventBus獲取到的;處理異常的SubscriberExceptionHandler型別也是通過EventBus獲取到的。(原來EventBus中的構造方法中的欄位是在這裡用到的!)至於反射觸發方法呼叫並沒有太複雜的邏輯。

另外還要注意下Subscriber還有一個字類SynchronizedSubscriber,它與一般的Subscriber的不同就在於它的反射觸發呼叫的方法被sychronized關鍵字修飾,也就是它的觸發方法是加鎖的、執行緒安全的。

總結:

至此,我們已經完成了EventBus的原始碼分析。簡單總結一下:

EventBus中維護了三個快取和四個對映:

  1. 事件型別到觀察者列表的對映(快取);
  2. 事件型別到監聽者方法列表的對映(快取);
  3. 事件型別到事件型別及其所有父類的型別的列表的對映(快取);
  4. 觀察者到監聽者的對映,觀察者到監聽方法的對映;

觀察者Subscriber內部封裝了監聽者和監聽方法,可以直接反射觸發。而如果是對映到監聽者的話,還要判斷監聽者的方法的型別來進行觸發。個人覺得這個設計是非常棒的,因為我們無需再在EventBus中維護一個對映的快取了,因為Subscriber中已經完成了這個一對一的對映。

每次使用EventBus註冊和取消註冊監聽者的時候,都會先從快取中進行獲取,不是每一次都會用到反射的,這可以提升獲取的效率,也解答了我們一開始提出的效率的問題。當使用反射觸發方法的呼叫貌似是不可避免的了。

最後,EventBus中使用了非常多的資料結構,比如MultiMap、CopyOnWriteArraySet等,還有一些快取和對映的工具庫,這些大部分都來自於Guava。

看了EventBus的實現,由衷地感覺Google的工程師真牛!而Guava中還有許多更加豐富的內容值得我們去挖掘!

瞭解執行緒區域性遍歷可以參考下我的另一篇博文:ThreadLocal的使用及其原始碼實現

相關文章