本文來自尚妝前端團隊路遠
發表於尚妝github部落格,歡迎訂閱!
EventBus
是基於觀察者模式的釋出/訂閱事件匯流排,它讓元件間的通訊變得更加簡單。類似廣播系統,不過 EventBus
所有的訂閱和傳送都是在記憶體層面的,使用起來遠比廣播簡單,也更容易管理。
先說明在事件匯流排中的幾個關鍵詞:
- 事件傳送者,發出事件的人
- 訂閱者,處理事件的人
- 訂閱者中處理事件的方法,因為每個訂閱者感興趣的事件有多種,因此會有多個處理事件的方法
- 訂閱,一個訂閱指的是某個訂閱者中的處理某個事件的方法,由訂閱者和事件型別唯一確定。
訂閱事件註冊
當希望接受到事件時,需要在 onCreate()
執行 register()
方法,在註冊方法中會檢索當前類中宣告的接受事件的方法,並將他們註冊到對應的對映中。
public void register(Object subscriber) {
Class<?> subscriberClass = subscriber.getClass();
List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}
複製程式碼
記憶體中儲存的資料結構有如下幾個:
// 事件 - List<訂閱(Subscription)> 每個訂閱由訂閱者、事件型別唯一確定
private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
// 訂閱者 - List<關注的事件> 每個訂閱者可能關注多個事件
private final Map<Object, List<Class<?>>> typesBySubscriber;
// 事件對應下的粘滯事件
private final Map<Class<?>, Object> stickyEvents;
複製程式碼
查詢訂閱方法列表
當執行 register()
方法時,會藉助 SubscriberMethodFinder
類從註冊的物件的 Class
中查詢。
List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
// 從快取中找是否已經檢索過了,有快取就直接返回
List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
if (subscriberMethods != null) {
return subscriberMethods;
}
// 是否忽略索引功能,忽略的話會直接使用反射的方法搜尋,否則會檢測有沒有相關的索引可以使用
if (ignoreGeneratedIndex) {
subscriberMethods = findUsingReflection(subscriberClass);
} else {
// 支援索引的情況,會優先從索引中查詢,加快查詢的速度
subscriberMethods = findUsingInfo(subscriberClass);
}
if (subscriberMethods.isEmpty()) {
// 沒有找到任何的訂閱方法將會丟擲異常,所以至少要用註解訂閱一個方法
} else {
// 針對這個 class 查詢到訂閱的方法列表,存快取,下次更快的返回
METHOD_CACHE.put(subscriberClass, subscriberMethods);
return subscriberMethods;
}
}
複製程式碼
因為我們不考慮索引的情況,最終查詢方法都會走到方法 findUsingReflectionInSingleClass
,內部的原理相對簡單,遍歷該類的所有方法,找到共有的、只有一個引數、且帶有 @Subscribe
註解的方法,儲存到列表中。
private static final int MODIFIERS_IGNORE = Modifier.ABSTRACT | Modifier.STATIC | BRIDGE | SYNTHETIC;
private void findUsingReflectionInSingleClass(FindState findState) {
Method[] methods;
methods = findState.clazz.getDeclaredMethods();
for (Method method : methods) {
int modifiers = method.getModifiers();
// 共有的方法 & 不是靜態、抽象、不是編譯生成的方法
if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
Class<?>[] parameterTypes = method.getParameterTypes();
// 引數長度只能是1
if (parameterTypes.length == 1) {
Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
// 方法上面帶有 @Subscribe 註解
if (subscribeAnnotation != null) {
Class<?> eventType = parameterTypes[0];
if (findState.checkAdd(method, eventType)) {
ThreadMode threadMode = subscribeAnnotation.threadMode();
findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
}
}
}
}
}
}
複製程式碼
這個過程是一個迴圈,每次都會向上查詢當前類的父類,知道到達 java
內建的類中,這就意味著,父類中宣告的訂閱方法,在子類例項中也會接收到。查詢的結果最終會生成一個 SubscriberMethod
的列表,這個類中儲存了訂閱方法的全部資訊,資料結構如下:
public class SubscriberMethod {
final Method method; // 當前的方法,可執行
final ThreadMode threadMode; // 執行緒型別
final Class<?> eventType; // 引數的型別,也就是他訂閱的事件的型別
final int priority; // 優先順序
final boolean sticky; // 是否是粘滯事件
String methodString; // 方法的字串
}
複製程式碼
訂閱到對映中
// 事件 - List<訂閱(Subscription)> 每個訂閱由訂閱者、事件型別唯一確定
private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
// 訂閱者 - List<關注的事件>
private final Map<Object, List<Class<?>>> typesBySubscriber;
複製程式碼
訂閱的過程就是根據訂閱者 Subscriber
及該訂閱者的某個處理事件的方法 SubscriberMethod
來生成 Subscription
並且儲存到對映當中。
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
// 儲存到 事件 - List<訂閱> 對映中
Class<?> eventType = subscriberMethod.eventType;
Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType); // ... 不存在則建立新的
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;
}
}
// 儲存到 訂閱者 - List<關注的事件> 對映中
List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber); // ... 不存在則建立新的
subscribedEvents.add(eventType);
// ...
// 對 Sticky Event 的處理,後面單獨說
}
複製程式碼
取消註冊
由於事件匯流排的機制基於記憶體實現,所有的訂閱都會儲存在記憶體中,因此必須在合適的時機取消註冊,來釋放佔用的記憶體空間。
當取消註冊時:
- 藉助之前儲存的
訂閱者-List<關注事件>
的對映快速的獲取到,當前訂閱者感興趣的事件列表。 - 然後遍歷事件列表,從
事件-List<訂閱>
的對映中,刪除所有的訂閱。 - 最後將當前訂閱者從
訂閱者-List<關注事件>
刪除,完成取消訂閱的過程。
獲取當前訂閱者關注的全部事件,遍歷取消註冊。
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 {
}
}
// 從訂閱列表中刪除對應的訂閱
private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {
List<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
if (subscriptions != null) {
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
的 post()
方法。
藉助 ThreadLocal
每個執行緒單獨維護一個、且僅一個 PostingThreadState
物件,這個物件的資料結構如下, 內部儲存了當前傳送事件狀態的的一些關鍵資訊。
final static class PostingThreadState {
final List<Object> eventQueue = new ArrayList<Object>(); // 事件佇列
boolean isPosting; // 是否正在傳送事件,是的話不需要啟動迴圈讀取事件
boolean isMainThread; // 是否是主執行緒
Subscription subscription; // 一個訂閱
Object event; // 當前的事件
boolean canceled; // 是否被取消
}
複製程式碼
獲取本執行緒的 PostingThreadState 物件,進行初始化,並開始輪詢處理佇列中的事件。
public void post(Object event) {
PostingThreadState postingState = currentPostingThreadState.get();
List<Object> eventQueue = postingState.eventQueue;
eventQueue.add(event);
if (!postingState.isPosting) {
postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();
postingState.isPosting = true;
try {
// 從佇列中迴圈讀取事件處理
while (!eventQueue.isEmpty()) {
postSingleEvent(eventQueue.remove(0), postingState);
}
} finally {
postingState.isPosting = false;
postingState.isMainThread = false;
}
}
}
複製程式碼
繼續往深裡面看 postSingleEvent()
方法,他每次處理一個從佇列中取出來的事件,這裡做了一個區分,是否支援繼承,這個值預設是 true
,支援繼承時,如果對當前事件的父類、介面對應的事件感興趣,那麼他也可以處理該事件。例如當前要處理 A 事件,A 繼承自 B,同時實現 C 介面,能處理 B,C 事件的訂閱者將也會參與處理此 A 事件。
private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
Class<?> eventClass = event.getClass();
boolean subscriptionFound = false;
if (eventInheritance) {
// 向父類搜尋,將父類、介面全部查詢到
List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
int countTypes = eventTypes.size();
for (int h = 0; h < countTypes; h++) {
Class<?> clazz = eventTypes.get(h);
subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
}
} else {
subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
}
if (!subscriptionFound) {
// 沒有找到訂閱的方法,處理分支
}
}
複製程式碼
事件訂閱者排隊處理
接下來會走 postSingleEventForEventType()
方法,這個方法負責找到對這個事件感興趣的 訂閱 Subscription 列表, Subscription
裡面包含了訂閱者、處理對應事件的方法等資訊。
拿到列表之後便迴圈將事件給列表中的訂閱依次處理,在之前註冊時,是有一個優先順序別的,優先順序高的將會先獲得處理事件的權利。
優先順序別較高的處理者可以停止事件的傳遞,只需要丟擲一個異常,被 finally
塊捕捉後,就會中斷輪詢,從而終止事件的傳遞。
private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?>
CopyOnWriteArrayList<Subscription> subscriptions;
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 {
// 讓 subscription 處理 event
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;
}
複製程式碼
分發執行緒處理者執行
處理事件的最後一步,是 postToSubscription()
他負責將事件的處理分發到不同的執行緒佇列中,在新增訂閱註解 @Subscribe
時可以指定 threadMode
,這極大的方便了我們在事件傳遞後切換不同執行緒處理事件,例如我們常常要在子執行緒處理資料,而通知主執行緒更新 UI
,使用 EventBus
只需要指定 @Subscribe(threadMode=ThreadMode.Main)
則在處理事件時所有操作在內部便被切換到了主執行緒,真正做到了對執行緒切換的無感知。
分為了如下幾種型別:
POSTING
傳送執行緒,或者說是當前執行緒更貼切一些,在其他類庫中通常叫Immediate
, 也就是不用切換執行緒。MAIN
主執行緒,不解釋。BACKGROUND
後臺執行緒,如果傳送執行緒是主執行緒,則開闢新的執行緒執行,否則將在當前執行緒執行。ASYNC
非同步執行緒,無論怎樣,總是開啟新的子執行緒去執行。
這裡就要看一下幾個處理者 HandlerPoster
/BackgroundPoster
/AsyncPoster
實現原理大致相同,內部維護一個佇列,不停的把裡面的事件取出來處理。
HandlerPoster
是基於Handler
實現對佇列的輪詢。BackgroundPoster
則是用死迴圈來做的,誰讓人家有自己的執行緒呢。AsyncPoster
就更富了,根本不輪詢,每次都是一個新的執行緒。
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 BACKGROUND:
if (isMainThread) {
backgroundPoster.enqueue(subscription, event);
} else {
invokeSubscriber(subscription, event);
}
break;
case ASYNC:
asyncPoster.enqueue(subscription, event);
break;
}
}
複製程式碼
最終呼叫的 invokeSubscriber()
很簡單就是利用反射調一下對應的 method
subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
複製程式碼
粘滯事件的實現
我把 Sticky Event
翻譯成 粘滯事件 不知道對不對,他的出現主要是因為我們需要處理事件是總是要先註冊再傳送事件,根本原因在於當一個事件發出時,他的生命週期很短,所有對他感興趣的訂閱者處理完了之後他就被拋棄了,後面的訂閱者再感興趣也沒用,因為早就被清理啦。
要解決這個問題也很簡單,就是延長事件的生命週期,即使大家都不理他了,他也能頑強的活著,萬一後面還有人對他感興趣呢。所以實現的原理也就很明瞭了,找個列表把它全部存起來,除非你手動給刪除,否則就 粘不拉幾 的附著在你的記憶體裡,等著他的真命天子出現。
// 事件型別 - 事件例項
private final Map<Class<?>, Object> stickyEvents;
// 傳送粘滯事件時,先存起來給後面的人用,然後按照常規流傳送出去
public void postSticky(Object event) {
synchronized (stickyEvents) {
stickyEvents.put(event.getClass(), event);
}
post(event);
}
複製程式碼
還要提供一個渠道,讓新加入進來的訂閱者能夠察覺到這裡有粘滯事件的存在,如果感興趣也可以處理它。這個時機就是註冊時,當一個訂閱者被新增到登錄檔中時,此時如果存在粘滯事件,用當前訂閱者感興趣的事件為 key
獲取存在的粘滯事件,如果有感興趣的就臨幸一下。於是可以完善一下之前未說完的 register()
方法:
- 首先要求當前訂閱者的處理事件的方法要對粘滯事件感興趣,這個在註解上可以宣告。
- 繼承,如果支援繼承,當前事件的子類粘滯事件都會被取出來檢查是否可以被處理。
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);
}
}
}
複製程式碼
接下來的 checkPostStickyEventToSubscription()
就會呼叫前面已經說過的 postToSubscription()
方法,開始傳送到不同的執行緒中執行,這部分和普通的事件是一樣的啦。
理解事件的繼承
粘滯事件這裡也出現了一個關於事件繼承的檢索,在上一節也出現了一次,單獨拿出來說一下異同之處。
可以類比函式入參的限制,如果一個方法宣告中引數是父類,那麼傳參時可以傳遞子類物件進去,宣告瞭子類的話,是不能傳遞父類物件的。
舉個例子,設定下場景,我們現在有事件基類 BaseEvent
和一個事件子類 ImplEvent
是繼承關係。
第一種場景,傳送普通事件,我傳送了一個 ImplEvent
,因為我發的是個子類事件,也就是說所有宣告關注 BaseEvent
的訂閱者也都可以將當前事件作為入參,所以向上檢索對 ImplEvent
父類、父介面感興趣的訂閱者去執行。
第二個場景,傳送粘滯事件,傳送一個 BaseEvent
的粘滯事件,因為是在註冊時觸發執行,那麼說明當前訂閱者對 BaseEvent
感興趣,既然他的入參是父類事件,那麼子類事件也同樣可以作為他的處理事件方法的入參,於是檢索所有粘滯事件找到所有 BaseEvent
的子類事件都交給當前訂閱者處理。
Weex 事件機制
在 Weex
中有一個 BroadcastChannel
的 API
用來實現頁面間的通訊,在原生部分使用 WebSocketModule
實現,不過經過實驗發現,註冊和傳送沒有什麼大問題,不過在取消註冊這塊做的有漏洞,出現多次頁面銷燬但是無法取消對事件監聽的情況(可能是當時嘗試的時候版本低一些),主要是因為 module
的生命週期沒能和 weex
頁面例項更好的繫結起來,而且它是基於 W3C
的標準設計的,也沒有實現類似粘滯事件這種功能的支援。
最後決定根據事件匯流排的機制來嘗試實現頁面之間的通訊,在 Weex
中有一個 頁面內 通訊的介面,他是 native
和 weex
通訊的通道,可以用一個 key
作為標示符,觸發當前 weex
頁面中對 key
事件感興趣的的方法,關於 weex
相關的內容這裡不細說。
((WXSDKInstance)instance).fireGlobalEventCallback(key, params)
複製程式碼
實現原理類似 EventBus
,不過因為基於 weex
就沒那麼複雜,同樣需要維護一個登錄檔,相對於 EventBus
要對訂閱者強引用持有,這裡使用了每個 weex
頁面唯一的 instanceId
作為標記,儲存這個標記而不是儲存真正的 WXSDKInstance
物件,避免記憶體洩漏。
private val mEventInstanceIdMap by lazy { mutableMapOf<String, MutableSet<String>>() }
複製程式碼
註冊,當 weex
那邊發起註冊時,拿到對應的 instanceId
儲存到對映中。
// 註冊接受某事件
// event.registerEvent('myEvent')
// globalEvent.addEventListener('myEvent', (params) => {});
fun registerEvent(key: String?, instantId: String?) {
// do check...
val nonNullKey = key ?: return
val registerInstantIds = mEventInstanceIdMap[nonNullKey] ?: mutableSetOf()
registerInstantIds.add(instantId)
mEventInstanceIdMap[nonNullKey] = registerInstantIds
}
複製程式碼
傳送事件時,根據事件的 key
拿到對他關注的訂閱者的 instanceId
列表,迴圈從 weex sdk
中取出真正的 WXSDKInstance
物件,再利用頁面內通訊的 API
將事件傳送給指定頁面,達到頁面間通訊的目的。
// 傳送事件
// event.post('myEvent',{isOk:true});
fun postEvent(key: String, params: Map<String, Any>) {
// do check...
val registerInstantIds = mEventInstanceIdMap[key] ?: listOf<String>()
val allInstants = renderManager.allInstances
for (instance in allInstants) {
// 遍歷找到訂閱的 instanceId 進而拿到 weex 例項傳送頁面內事件
if (instance != null
&& !instance.instanceId.isNullOrEmpty()
&& registerInstantIds.contains(instance.instanceId)) {
instance.fireGlobalEventCallback(key, params)
}
}
}
複製程式碼
當頁面銷燬時,同時自動取消註冊,釋放記憶體和避免不必要的事件觸發
override fun onWxInstRelease(weexPage: WeexPage?, instance: WXSDKInstance?) {
val nonNullId = instance?.instanceId ?: return
for (mutableEntry in mEventInstanceIdMap) {
if (mutableEntry.value.isNotEmpty()) {
mutableEntry.value.remove(nonNullId)
}
}
}
複製程式碼
最後,目前只是一個簡單的實現,能夠基本實現頁面間通訊的需求,不過還需要更多地調研和其他端同學的配合,相信會越來越完善。
目前維護的幾個專案,求 ✨✨✨✨