Guava 源碼分析之 EventBus 源碼分析

EventBus的設計理念是基於觀察者模式的,能夠參考設計模式(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註解聲明而且參數與發佈的事件類型匹配,那麼這個方法纔會被觸發。這就是說,同一個監聽者能夠監聽多種類型的事件,也能夠在屢次監聽同一個事件。安全

二、EventBus源碼分析

2.1 分析以前

好了,經過上面的例子,咱們瞭解了EventBus最基本的使用方法。下面咱們來分析一下在Guava中是如何爲咱們實現這個API的。不過,首先,咱們仍是先試着考慮一下本身設計這個API的時候如何設計,而且提出幾個問題,而後帶着問題到源碼中尋找答案。數據結構

假如要咱們去設計這樣一個API,最簡單的方式就是在觀察者模式上進行拓展:每次調用EventBus.post()方法的時候,會對全部的觀察者對象進行遍歷,而後獲取它們所有的方法,判斷該方法是否使用了@Subscribe而且方法的參數類型是否與post()方法發佈的事件類型一致,若是一致的話,那麼咱們就使用反射來觸發這個方法。在觀察者模式中,每一個觀察者都要實現一個接口,發佈事件的時候,咱們只要調用接口的方法就行,可是EventBus把這個限制設定得更加寬泛,也就是監聽者無需實現任何接口,只要方法使用了註解而且參數匹配便可。異步

從上面的分析中能夠看出,這裏面不只要對全部的監聽者進行遍歷,還要對它們的方法進行遍歷,找到了匹配的方法以後又要使用反射來觸發這個方法。首先,當註冊的監聽者數量比較多的時候,鏈式調用的效率就不高;而後咱們又要使用反射來觸發匹配的方法,這樣效率確定又低了一些。那麼在GuavaEventBus中是如何解決這兩個問題的?ide

另外還要注意下下文中的觀察者監聽者的不一樣,監聽者用來指咱們使用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,固然咱們也能夠定義本身的異常處理策咯。post

因此,從上面的分析中能夠看出,若是咱們想要了解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的使用及其源碼實現

相關文章
相關標籤/搜索