聊一聊 EventBus 原始碼和設計之禪

揪克發表於2018-04-15

歡迎關注本人公眾號,掃描下方二維碼或搜尋公眾號 id: mxszgg

聊一聊 EventBus 原始碼和設計之禪

前言

筆者看過一些知名開源專案的原始碼,認為 EventBus 算是其中最簡單的,甚至複雜程度不在一個級別上。解析原始碼前先提一下以下幾個變數和類,掌握了這些變數和類基本上 EventBus 已經就掌握一半了。

  • METHOD_CACHEMap<Class<?>, List<SubscriberMethod>> 型別。鍵為註冊類的 Class,值為該類中所有 EventBus 回撥的方法連結串列(也就是被 @Subscribe 標記的方法們)。
  • typesBySubscriberMap<Object, List<Class<?>>> 型別。鍵為物件本身(例如 Activity 物件),值為該物件中所有的 Event 的類型別。該欄位只用於僅用於判斷某個物件是否註冊過,在日常使用中幾乎沒什麼作用(感謝評論區指出)。
  • Subscription 類(文中稱訂閱資訊):關注類中兩個欄位,一個是 Object 型別的 subscriber,該欄位即為註冊的物件(在 Android 中時常為 Activity);另一個是 SubscriberMethod 型別的 subscriberMethod,細節如下:
    • subscriberMethodSubscriberMethod 型別(文中稱訂閱方法)。關注類中有個欄位 eventTypeClass<?> 型別,代表 Event 的類型別。
  • subscribtionsByEventTypeMap<Class<?>, CopyonWriteArrayList<Subscribtion>> 型別。鍵為 Event 的類型別,值為元素為 Subscription(訂閱資訊)連結串列。核心欄位。

聊一聊 EventBus 原始碼和設計之禪

register()

直接檢視 EventBus#register() 原始碼:

public void register(Object subscriber) {
    Class<?> subscriberClass = subscriber.getClass();
    // 根據當前註冊類獲取 List<SubscriberMethod>
    List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
    synchronized (this) {
        for (SubscriberMethod subscriberMethod : subscriberMethods) {
        	// subsciber 對 List<SubscriberMethod> 中每個 SubscriberMethod 進行訂閱
            subscribe(subscriber, subscriberMethod);
        }
    }
}
複製程式碼

獲取當前註冊物件所有訂閱方法資訊

先檢視如何根據當前註冊類獲取 List 的,SubscriberMethodFinder#findSubscriberMethods(Class<?> subscriberClass) 原始碼精簡如下:

List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
    List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
    // 如果已存在則返回
    if (subscriberMethods != null) {
        return subscriberMethods;
    }

    subscriberMethods = findUsingReflection(subscriberClass);
    METHOD_CACHE.put(subscriberClass, subscriberMethods);
    
    return subscriberMethods;
}
複製程式碼

METHOD_CACHE 前面提到過,是存著註冊類與其所有需要回撥的 Event 方法列表的鍵值對。如果已經存在則直接返回,如果否則需要通過 findUsingReflection(subscriberClass) 方法進行查詢再返回,當然,返回之前需要存入 METHOD_CACHE 中,否則該 METHOD_CACHE 就沒有存在的意義了。

SubscriberMethodFinder#findUsingReflection() 原始碼如下:

private List<SubscriberMethod> findUsingReflection(Class<?> subscriberClass) {
    FindState findState = prepareFindState();
    findState.initForSubscriber(subscriberClass);
    while (findState.clazz != null) {
    	// 通過純反射去獲取被 @Subscribe 所修飾的方法
        findUsingReflectionInSingleClass(findState);
        // 將當前 class 的父類 class 賦值給 findState.clazz 
        findState.moveToSuperclass();
    }
    // 重置 FindState 便於下一次回收利用
    return getMethodsAndRelease(findState);
}    
複製程式碼

初始化 FindState 物件後,會進入一個 while 迴圈中,不停地去反射獲取當前類和其父類(注意,在 Java 中,如果當前類實現了一個介面,即使該介面的方法被 @Subscribe 所修飾,當前類中的方法也是不包含該註解屬性的,所以如果在介面中對某個方法使用了 @Subscribe 修飾然後讓類去實現這個介面是沒有任何作用的)的訂閱方法並添入列表中,最終返回這個列表並重置 FindState 物件利於下一次重複使用。反射獲取當前類和其父類的訂閱方法原始碼簡化如下:

private void findUsingReflectionInSingleClass(FindState findState) {
    Method[] methods;
    try {
    	// 返回當前類自身方法和顯式過載的父類方法
        methods = findState.clazz.getDeclaredMethods();
    } catch (Throwable th) {
        methods = findState.clazz.getMethods();
        findState.skipSuperClasses = true;
    }
    for (Method method : methods) {
        int modifiers = method.getModifiers();
        if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
            Class<?>[] parameterTypes = method.getParameterTypes();
            if (parameterTypes.length == 1) {
                Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
                if (subscribeAnnotation != null) {
                    Class<?> eventType = parameterTypes[0];
                    // needCheck
                    if (findState.checkAdd(method, eventType)) {
                        ThreadMode threadMode = subscribeAnnotation.threadMode();
                        findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
                                subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
                    }
                }
            }
        }
    }
}
複製程式碼

這裡想要提及的一點事,獲取到 @Subscribe 修飾的目標方法後,並非無腦地添入 subscriberMethods 中,而實際上是需要過濾一遍的,講解 checkAdd() 原始碼前,希望讀者思考以下幾個問題:

  • 對於同一個 Event,當前類對該物件使用了多個方法進行了多次訂閱,那麼如果該 Event 被髮射的時候,當前類會如何呼叫這些方法?
  • 對於同一個 Event,父類對該物件進行了一次訂閱,子類重寫該訂閱方法,那麼如果該 Event 被髮射的時候,父類子類當中會如何處理這些方法?

解決這些方法就需要去看看 checkAdd() 的底層實現了——

boolean checkAdd(Method method, Class<?> eventType) {
    Object existing = anyMethodByEventType.put(eventType, method);
    if (existing == null) {
        return true;
    } else {
        return checkAddWithMethodSignature(method, eventType);
    }
}
複製程式碼

可以看到 anyMethodByEventType 使用了 Event 的 Class 作為鍵,這像是意味著一個類對於同一個 Event 只能訂閱一次,事實上是不是這樣,還得繼續看看 checkAddWithMethodSignature(),其原始碼簡化如下:

private boolean checkAddWithMethodSignature(Method method, Class<?> eventType) {
    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)) {
        return true;
    } else {
        subscriberClassByMethodKey.put(methodKey, methodClassOld);
        return false;
    }
}
複製程式碼

可以看到 subscriberClassByMethodKey 使用方法名 + '>' + 事件型別作為鍵,這意味著對於同一個類來說,subscriberClassByMethodKey 肯定不會鍵重複(畢竟一個類中不能夠方法名相同且方法引數、個數都相同),因此它最終會返回 true。這意味著一個類如果使用了多個方法對同一個 Event 物件進行註冊,那麼當該 Event 物件被髮射出來的時候,所有的方法都將會得到回撥。

但是當父類執行上述操作的時候,如果子類有「顯示」實現父類的訂閱方法,那麼此時 subscriberClassByMethodKey.put(methodKey, methodClass) 返回值不會為空,且為子類的 Class,此時 if 上分支將會判斷子類 Class 是否 isAssignableFrom 父類 Class,這肯定是會為 false 的,這將會走入 if 下分支並返回 false。這意味著當子類「顯示」實現父類的訂閱方法的時候,如果此時發射指定 Event 的話,父類的訂閱方法將不會執行,而僅會執行子類的訂閱方法。

subscribe()

獲取到相應的 SubscriberMethod 連結串列後,就是對連結串列中的 SubscriberMethod 物件進行訂閱了,EventBus#subscribe() 方法原始碼精簡如下:

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
    Class<?> eventType = subscriberMethod.eventType;
    Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
    CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
    if (subscriptions == null) {
        subscriptions = new CopyOnWriteArrayList<>();
        subscriptionsByEventType.put(eventType, subscriptions);
    }

    int size = subscriptions.size();
    for (int i = 0; i <= size; i++) {
        if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
        	// 根據 priority 大小放入 List 中
            subscriptions.add(i, newSubscription);
            break;
        }
    }

    List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
    if (subscribedEvents == null) {
        subscribedEvents = new ArrayList<>();
        typesBySubscriber.put(subscriber, subscribedEvents);
    }
    subscribedEvents.add(eventType);

    // 省略 sticky 事件
}
複製程式碼

subscriptionsByEventType 根據 Event 事件類型別獲取訂閱資訊連結串列,當然,如果沒有的話那就 new 一個並放入其中。接著根據訂閱方法的優先順序塞入該連結串列中。最後 typesBySubscriber 獲取該 subsciber 的所有 Event 事件型別連結串列,並新增當前 Event 事件型別。關於 sticky 事件的具體內容在 sticky 中會具體講解。

至此 EventBus#register(Object) 方法算是結束了。

聊一聊 EventBus 原始碼和設計之禪

post()

EventBus#post(Object) 原始碼精簡如下:

public void post(Object event) {
    PostingThreadState postingState = currentPostingThreadState.get();
    List<Object> eventQueue = postingState.eventQueue;
    eventQueue.add(event);

	// 確保不會被呼叫多次
    if (!postingState.isPosting) {
        postingState.isMainThread = isMainThread();
        postingState.isPosting = true;
        try {
            while (!eventQueue.isEmpty()) {
            	// 分發 Event 事件
                postSingleEvent(eventQueue.remove(0), postingState);
            }
        } finally {
        	// 最後要 reset flag
            postingState.isPosting = false;
            postingState.isMainThread = false;
        }
    }
}
複製程式碼

currentPostingThreadState 是一個 ThreadLocal 類,通過它獲取到 PostingThreadState 物件,再根據該物件獲取到 event 連結串列(有沒有聯想到 Android 中的訊息機制?),並將傳入的 event 塞入該連結串列。為了控制 Event 出佇列不會被呼叫多次,PostingThreadState 物件有一個 isPosting 來標記當前連結串列是否已經開始進行回撥操作,通過原始碼可以看到,每次分發完一個 Event 事件,該事件也會被從連結串列中 remove 出去。

postSingleEvent()

具體 postSingleEvent() 原始碼精簡如下:

private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
    Class<?> eventClass = event.getClass();
    postSingleEventForEventType(event, postingState, eventClass);
}
複製程式碼

追溯 EventBus#postSingleEventForEventType() 原始碼精簡如下:

private void postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
    CopyOnWriteArrayList<Subscription> subscriptions;
    synchronized (this) {
        subscriptions = subscriptionsByEventType.get(eventClass);
    }
    if (subscriptions != null && !subscriptions.isEmpty()) {
        for (Subscription subscription : subscriptions) {
            postingState.event = event;
            postingState.subscription = subscription;
            try {
                postToSubscription(subscription, event, postingState.isMainThread);
            } finally {
                postingState.event = null;
                postingState.subscription = null;
                postingState.canceled = false;
            }
        }
    }
}
複製程式碼

通過 subscriptionsByEventType 獲取該 Event 事件對應的訂閱資訊連結串列,然後將該訂閱資訊Event 和當前執行緒資訊傳給了 postToSubscription() 方法,該方法戳進去一看就知道是用來去回撥所有訂閱方法的,該方法的具體分析在 threadMode 中。實際上到這裡 post() 流程就算是結束了。所以實際上核心方法 post() 的原始碼是十分簡單的,也可以看得到,核心欄位也僅有 subscriptionsByEventType 一個而已。

聊一聊 EventBus 原始碼和設計之禪

unregister()

EventBus#unregister(Object) 方法原始碼精簡如下:

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

整體看來分兩步走,一步是移除註冊物件和其所有 Event 事件連結串列,即 typesBySubscriber 移除相關鍵值對的;再就是在 unsubscribeByEventType() 方法中對 subscriptionsByEventType 移除了該 subscriber 的所有訂閱資訊(可以看到實際上沒有對 METHOD_CACHE 進行相關移除操作,便於下一次註冊的時候可以很方便拿到之前的資訊,這便是快取的作用所在)。

threadMode

在 EventBus 中,共有四種 threadMode,如下:

public enum ThreadMode {
    POSTING,

    MAIN,

    MAIN_ORDERED,

    BACKGROUND,

    ASYNC
}
複製程式碼
  • POSTING:接收事件方法應執行在發射事件方法所在的執行緒(由於發射事件方法執行緒可能是主執行緒,這意味著接收方法不能執行耗時操作,否則會阻塞主執行緒)
  • MAIN:在 Android 中則接收事件方法應執行在主執行緒,否則(在 Java 專案中)等同於 POSTING。如果發射事件方法已位於主執行緒,那麼接收事件方法會被「立即」呼叫(這意味著接收事件方法不能執行耗時操作,否則會阻塞主執行緒;同時,由於是「立即」呼叫,所以發射事件方法此時是會被接收事件方法所阻塞的),否則等同於 MAIN_ORDERED
  • MAIN_ORDERED:在 Android 中則接收事件方法會被扔進 MessageQueue 中等待執行(這意味著發射事件方法是不會被阻塞的),否則(在 Java 專案中)等同於 POSTING
  • BACKGROUND
    • 在 Android 中
      • 發射事件方法在主執行緒中執行,則接收事件方法應執行在子執行緒執行,但該子執行緒是 EventBus 維護的單一子執行緒,所以為了避免影響到其他接收事件方法的執行,該方法不應太耗時避免該子執行緒阻塞。
      • 發射事件方法在子執行緒中執行,則接收事件方法應執行在發射事件方法所在的執行緒。
    • 在 Java 專案中,接收事件方法會始終執行在 EventBus 維護的單一子執行緒中。
  • ASYNC:接收方法應執行在不同於發射事件方法所在的另一個執行緒。常用於耗時操作,例如網路訪問。當然,儘量避免在同一個時間大量觸發此型別方法,儘管 EventBus 為此專門建立了執行緒池來管理回收利用這些執行緒。

關於以上 threadMode 哪幾種應避免耗時操作,耗時時阻塞的是哪條執行緒,希望各位讀者能夠仔細閱讀。

說完幾種 threadMode 之後,再來看看前文遺留下來的問題——postToSubscription() 原始碼如下:

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 MAIN_ORDERED:
            if (mainThreadPoster != null) {
                mainThreadPoster.enqueue(subscription, event);
            } else {
                // temporary: technically not correct as poster not decoupled from subscriber
                invokeSubscriber(subscription, event);
            }
            break;
        case BACKGROUND:
            if (isMainThread) {
                backgroundPoster.enqueue(subscription, event);
            } else {
                invokeSubscriber(subscription, event);
            }
            break;
        case ASYNC:
            asyncPoster.enqueue(subscription, event);
            break;
        default:
            throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
    }
}
複製程式碼

細看原始碼,其實可以發現只用到了兩種方法,一種是 invokeSubscriber 意味著立即呼叫該方法,另一種是 xxxPoster.enqueue() 意味著需要使用其他執行緒來執行該方法。

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() 方法呢?

  • POSTING:不用說,既然和發射事件執行緒同一條執行緒執行,那麼當然直接呼叫 invokeSubscriber() 即可。
  • MAIN:在確保發射事件執行緒是主執行緒的情況下,直接呼叫 invokeSubscriber()
  • MAIN_ORDERED:如果當前專案不是 Android 專案情況下(純 Java 專案),將會直接呼叫 invokeSubscriber()
  • BACKGROUND:前面提到如果發射事件執行緒不是主執行緒的話,接收事件將會執行於發射事件所在的執行緒,所以也會直接呼叫 invokeSubscriber()

文中已多次提到 Android 專案和純 Java 專案,是由於在 Java 專案中大部分情況下不需要特地區分主執行緒和子執行緒(這一點筆者也得到了女票的證實)。其實不僅是 EventBus,RxJava 也是如此,RxJava 中是沒有 Schedulers.mainThread() 一說的,僅有 Schedulers.trampoline() 表當前執行緒。

聊一聊 EventBus 原始碼和設計之禪

Poster#enqueue()

根據原始碼可以看出來分為以下三種:

這裡寫圖片描述

HandlerPoster 原始碼不在此擴充套件了,熟悉 Android 的讀者們應該都猜得到 HandlerPoster 底層實現肯定是通過 Handler 機制來實現的,HandlerPoster#enqueue() 方法的實現離不開 Hanlder#sendMessage(),而處理方式肯定就是在 Hanlder#handleMessage() 中去呼叫 invokeSubscriber()

BackgroundPoster 原始碼也不在此擴充套件了,前面提到 EventBus 會維護單一執行緒去執行接收事件方法,所以肯定會在 Runnable#run() 中去呼叫 invokeSubscriber()

AsyncPoster 的底層實現實際上與 BackgroundPoster 大同小異,但是有讀者會疑惑了,BackgroundPoster 底層維護的是「單一」執行緒,而 AsyncPoster 肯定不是這樣的啊。這裡的細節留到設計技巧一節再來細說。

sticky

什麼叫做 sticky 事件筆者此處就不做擴充套件了。專案中如果想要發射 sticky 事件需要通過 EventBus#postSticky() 方式,原始碼如下:

public void postSticky(Object event) {
    synchronized (stickyEvents) {
        stickyEvents.put(event.getClass(), event);
    }
    post(event);
}
複製程式碼

可以看到第一步是將該事件放入 stickyEvents 中,第二步則是正常 post()。為避免多執行緒操作 postSticky(Object)removeStickyEvent(Class<?>) 引發的衝突,所以對 stickyEvents 物件新增了 synchronized 關鍵字,不得不說 EventBus 作者的設計實在是縝密啊。前文提到 EventBus#register() 中關於 sticky 事件的程式碼簡化如下:

if (subscriberMethod.sticky) {
    Object stickyEvent = stickyEvents.get(eventType);
    if (stickyEvent != null) {
        postToSubscription(newSubscription, stickyEvent, isMainThread());
    }
}
複製程式碼

可以看到,沒有什麼特殊的地方,判斷當前事件是否 sticky,如果 sticky 則從 stickyEvents 拿出該事件並執行 postToSubscription() 方法。

優化操作

eventInheritance

不知道各位讀者在日常使用 EventBus 中會不會在 Event 之間存在繼承關係,反正筆者是沒這樣用過。也正是存在筆者這種不會這樣使用 Event 和會使用 Event 繼承的開發者之間的矛盾才會有這個欄位出現。全域性搜尋該欄位僅用於發射事件的時候判斷是否需要發射父類事件,由於該欄位預設為 true,所以如果各位讀者和筆者一樣在專案開發中 Event 不存在繼承關係的話,可以將該欄位設為 false 以提高效能。

APT

EventBus 內部使用了大量的反射去尋找接收事件方法,實際上有經驗的小夥伴知道可以使用 APT 來優化。這也就是 EventBus 3.0 引入的技術,此處的使用便不在此處擴充套件了,程式碼中通過 ignoreGeneratedIndex 來判斷是否使用生成的 APT 程式碼去優化尋找接收事件的過程,如果開啟了的話,那麼將會通過 subscriberInfoIndexes 來快速得到接收事件方法的相關資訊。所以各位讀者如果沒有在專案中接入 EventBus 的 APT,那麼可以將 ignoreGeneratedIndex 設為 false 提高效能。

聊一聊 EventBus 原始碼和設計之禪

設計技巧

反射方法

EventBus 在獲取接收事件方法的資訊中,通過 getDeclaredMethods() 來獲取類中所有方法而並不是通過 getMethods(),由於前者只反射當前類的方法(不包括隱式繼承的父類方法),所以前者的效率較後者更高些。

FindState

以下程式碼是 FindState 的獲取:

private FindState prepareFindState() {
    synchronized (FIND_STATE_POOL) {
        for (int i = 0; i < POOL_SIZE; i++) {
            FindState state = FIND_STATE_POOL[i];
            if (state != null) {
                FIND_STATE_POOL[i] = null;
                return state;
            }
        }
    }
    return new FindState();
}
複製程式碼

以下程式碼是 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;
}
複製程式碼

可以看到,EventBus 使用 FindState 並不是簡單的 new,由於 FindState 在註冊流程中使用頻繁且建立耗費資源,故建立 FindState 池複用 FindState 物件,與此相同的還有 PendingPost,它用於反射呼叫接收事件方法,具體不在此擴充套件。

AsyncPoster、BackgroundPoster

前面提到 AsyncPosterBackgroundPoster 的底層實現是一樣的,但是有讀者會疑惑了,BackgroundPoster 底層維護的是「單一」執行緒,而 AsyncPoster 肯定不是這樣的啊——筆者也是讀了原始碼之後才發現被 EventBus 作者擺了一道——在預設情況下實際上兩者底層維護的都是 Executors.newCachedThreadPool(),這是一個有則用、無則建立、無數量上限的執行緒池。而 BackgroundPoster 是如何控制「單一」的呢?其在 Executor#execute() 上新增了 synchronized 並設立 flag,保證任一時間只且僅能有一個任務會被執行緒池執行;而 AsyncPoster 只需無腦地將傳來的任務塞入執行緒池即可。

聊一聊 EventBus 原始碼和設計之禪

後記

EventBus 原始碼雖簡單,但是當中的很多設計技巧是非常值得學習的,例如前文提到的複用池,以及遍佈 EventBus 原始碼各處的 synchronized 關鍵字。希望各位讀者也能夠深入到其中去探索一番,尋找到筆者未找到的寶藏。

聊一聊 EventBus 原始碼和設計之禪

另外,我建了一個群,如果你對文章有什麼疑問,或者想和我討論 Android 技術,歡迎入群哈。

聊一聊 EventBus 原始碼和設計之禪

相關文章