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把這個限制設定得更加寬泛,也就是監聽者無需實現任何介面,只要方法使用了註解並且引數匹配即可。
從上面的分析中可以看出,這裡面不僅要對所有的監聽者進行遍歷,還要對它們的方法進行遍歷,找到了匹配的方法之後又要使用反射來觸發這個方法。首先,當註冊的監聽者數量比較多的時候,鏈式呼叫的效率就不高;然後我們又要使用反射來觸發匹配的方法,這樣效率肯定又低了一些。那麼在Guava
的EventBus
中是如何解決這兩個問題的?
另外還要注意下下文中的觀察者
和監聽者
的不同,監聽者用來指我們使用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實現,分別用於不同場景的事件分發:
ImmediateDispatcher
:直接在當前執行緒中遍歷所有的觀察者並進行事件分發;LegacyAsyncDispatcher
:非同步方法,存在兩個迴圈,一先一後,前者用於不斷往全域性的佇列中塞入封裝的觀察者物件,後者用於不斷從佇列中取出觀察者物件進行事件分發;實際上,EventBus有個字類AsyncEventBus就是用該分發器進行事件分發的。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中維護了三個快取和四個對映:
- 事件型別到觀察者列表的對映(快取);
- 事件型別到監聽者方法列表的對映(快取);
- 事件型別到事件型別及其所有父類的型別的列表的對映(快取);
- 觀察者到監聽者的對映,觀察者到監聽方法的對映;
觀察者Subscriber內部封裝了監聽者和監聽方法,可以直接反射觸發。而如果是對映到監聽者的話,還要判斷監聽者的方法的型別來進行觸發。個人覺得這個設計是非常棒的,因為我們無需再在EventBus中維護一個對映的快取了,因為Subscriber中已經完成了這個一對一的對映。
每次使用EventBus註冊和取消註冊監聽者的時候,都會先從快取中進行獲取,不是每一次都會用到反射的,這可以提升獲取的效率,也解答了我們一開始提出的效率的問題。當使用反射觸發方法的呼叫貌似是不可避免的了。
最後,EventBus中使用了非常多的資料結構,比如MultiMap、CopyOnWriteArraySet等,還有一些快取和對映的工具庫,這些大部分都來自於Guava。
看了EventBus的實現,由衷地感覺Google的工程師真牛!而Guava中還有許多更加豐富的內容值得我們去挖掘!
瞭解執行緒區域性遍歷可以參考下我的另一篇博文:ThreadLocal的使用及其原始碼實現