美團組件化事件匯流排方案改進:ModularEventBus

請點贊關注,你的支援對我意義重大。

🔥 Hi,我是小彭。本文已收錄到 GitHub · AndroidFamily 中。這裡有 Android 進階成長知識體系,有志同道合的朋友,關注公眾號 [彭旭銳] 帶你建立核心競爭力。

前言

大家好,我是小彭。2 年前,我們在 為了組件化改造學習十幾家大廠的技術部落格 這篇文章里收集過各大廠的組件化方案。其中,有美團收銀團隊分享的組件化匯流排框架 modular-event 讓我們印象深刻。然而,美團並未將該框架開源,我們只能望梅止渴。

在學習和借鑒美團 modular-event 方案中很多優秀的設計思想後,我亦發現方案中依然存在不一致風險和不足,故我決定對方案進行改進並向社區開源。項目主頁為 Github · ModularEventBus,演示 Demo 可直接下載:
Demo apk

歡迎提 Issue 幫助修復缺陷,歡迎提 Pull Request 增加新的 Feature,有用請點贊給 Star,給小彭一點創作的動力,謝謝。


這篇文章是 組件化系列文章第 5 篇,相關 Android 工程化專欄完整文章列表:

一、Gradle 基礎:

二、AGP 插件:

三、組件化開發:

四、AOP 面向切面編程:

五、相關電腦基礎


1. 認識事件匯流排

1.1 事件匯流排的優點

事件匯流排框架最大的優點是 」解耦「,即事件發布者與事件訂閱者的解耦,事件的發布者不需要關心是否有人訂閱該事件,也不需要關心是誰訂閱該事件,程式碼耦合度較低。因此,事件匯流排框架更適合作為全局的事件通訊方案,或者組件間通訊的輔助方案。

1.2 事件匯流排的缺點

然而,成也蕭何敗蕭何。有人覺得事件匯流排好用,亦有人覺得事件匯流排不好用,歸根結底還是因為事件匯流排太容易被濫用了,用時一時爽,維護火葬場。我將事件匯流排框架存在的問題概括為以下 5 種常見問題:

  • 1、消息難溯源: 在閱讀源碼的過程中,如果需要查找發布事件或訂閱事件的地方,只能通過查找事件引用的方式進行溯源,增大了理解程式碼邏輯的難度。特別是當項目中到處是臨時事件時,難度會大大增加;

  • 2、臨時事件濫用: 由於框架對事件定義沒有強制約束,開發者可以隨意地在項目的各個角落定義事件。導致整個項目都是臨時事件飛來飛去,增大後期維護的難度;

  • 3、數據類型轉換錯誤: LiveDataBus 等事件匯流排框架需要開發者手動輸入事件數據類型,當訂閱方與發送方使用不同的數據類型時,會發生類型轉換錯誤。在發生事件命名衝突時,出錯的概率會大大增加,存在隱患;

  • 4、事件命名重複: 由於框架對事件命名沒有強制約束,不同組件有可能定義重名的事件,產生邏輯錯誤。如果重名的事件還使用了不同的數據類型,還會出現類型轉換錯誤,存在隱患;

  • 5、事件命名疏忽: 與 」事件命名重複「 類似,由於框架對事件命名沒有檢查,有可能出現開發者複製粘貼後忘記修改事件變數值的問題,或者變數值拼寫錯誤(例如 login_success 拼寫為 login_succese),那麼訂閱方將永遠收不到事件。

1.3 ModularEventBus 的解決方案

ModularEventBus 組件化事件匯流排框架的優點是: 在保持發布者與訂閱者的解耦的優勢下,解決上述事件匯流排框架中存在的通病。 具體通過以下 5 個手段實現:

  • 1、事件聲明聚合: 發布者和訂閱者只能使用預定義的事件,嚴格禁止使用臨時事件,事件需要按照約定聚合定義在一個文件中(解決臨時事件濫用問題);

  • 2、區分不同組件的同名事件: 在定義事件時需要指定事件所屬 moduleName,框架自動使用 "[moduleName]$$[eventName]" 作為最終的事件名(解決事件命名重複問題);

  • 3、事件數據類型聲明: 在定義事件時需要指定事件的數據類型,框架自動使用該數據類型發送和訂閱事件(解決數據類型轉換錯誤問題);

  • 4、介面強約束: 運行時使用事件類發布和訂閱事件,框架自動使用事件定義的事件名和數據類型,而不需要手動輸入事件名和數據類型(解決事件命名命名錯誤);

  • 5、APT 生成介面類: 框架在編譯時使用 APT 註解處理器自動生成事件介面類。

1.4 與美團 modular-event 對比有哪些什麼不同?

  • modular-event 使用靜態常量定義事件,為什麼 ModularEventBus 用介面定義事件?

    美團 modular-event 使用常量引入了重複資訊,存在不一致風險。例如開發者複製一行常量後,只修改常量名但忘記修改值,這種錯誤往往很難被發現。而 ModularEventBus 使用方法名作為事件名,方法返回值作為事件數據類型,不會引入重複資訊且更加簡潔。

modular-event 事件定義

  • modular-event 使用動態代理,為什麼 ModularEventBus 不需要?

    美團 modular-event 使用動態代理 API 統一接管了事件的發布和訂閱,但考慮到這部分代理邏輯非常簡單(獲取事件名並交給 LiveDataBus 完成後續的發布和訂閱邏輯),且框架本身已經引入了編譯時 APT 技術,完全可以在編譯時生成這部分代理邏輯,沒必要使用動態代理 API。

  • 更多特性支援:

    此外 ModularEventBus 還支援生成事件文檔、空數據攔截、泛型事件、自動清除空閑事件等特性。


2. ModularEventBus 能做什麼?

ModularEventBus 是一款幫助 Android App 解決事件匯流排濫用問題的框架,亦可作為組件化基礎設施。 其解決方案是通過註解定義事件,由編譯時 APT 註解處理器進行合法性檢查和自動生成事件介面,以實現對事件定義、發布和訂閱的強約束。

2.1 常見事件匯流排框架對比

以下從多個維度對比常見的事件匯流排框架( ✅ 良好支援、✔️ 支援、❌ 不支援):

事件匯流排 ModularEventBus modular-event SmartEventBus LiveEventBus LiveDataBus EventBus RxBus
開發者 @彭旭銳 @美團 @JeremyLiao @JeremyLiao / @greenrobot /
Github Star 0 未開源 146 3.4k / 24.1k /
生成事件文檔
空數據攔截
無數據事件 ✔️
泛型事件 ✔️ ✔️
自動清除空閑事件
事件強約束 ✔️ ✔️
生命周期感知
延遲發送事件
有序接收事件
訂閱 Sticky 事件
清除 Sticky 事件
移除事件
執行緒調度
跨進程 / 跨 App ❌(可支援)
關鍵原理 APT+靜態代理 APT+動態代理 APT+靜態代理 LiveData LiveData APT RxJava

2.2 ModularEventBus 特性一覽

1、事件強約束

✅ 支援零配置快速使用;

✅ 支援 APT 註解處理器自動生成事件介面類;

✅ 支援編譯時合法性校驗和警告提示;

✅ 支援生成事件文檔;

✅ 支援增量編譯;

2、Lifecycle 生命周期感知

✅ 內置基於 LiveData 的 LiveDataBus;

✅ 支援自動取消訂閱,避免記憶體泄漏;

✅ 支援安全地發送事件與接收事件,避免產生空指針異常或不必要的性能損耗;

✅ 支援永久訂閱事件;

✅ 支援自動清除沒有關聯訂閱者的空閑 LiveData 以釋放記憶體;

3、更多特性支援

✅ 支援 Java / Kotlin;

✅ 支援 AndroidX;

✅ 支援訂閱 Sticky 粘性事件,支援移除事件;

✅ 支援 Generic 泛型事件,如 List<String> 事件;

✅ 支援攔截空數據;

✅ 支援只發布事件不攜帶數據的無數據事件;

✅ 支援延遲發送事件;

✅ 支援有序接收事件。


3. ModularEventBus 快速使用

  • 1、添加依賴

模組級 build.gradle

plugins {
    id 'com.android.application' // 或 id 'com.android.library'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}
dependencies {
    // 替換成最新版本
    implementation 'io.github.pengxurui:modular-eventbus-api:1.0.4'
    kapt 'io.github.pengxurui:modular-eventbus-compiler:1.0.4'
    ...
}
  • 2、定義事件數據類型(可選): 定義事件關聯的數據類型,對於只發布事件而不需要攜帶數據的場景,可以不定義事件類型。

UserInfo.kt

data class UserInfo(val userName: String)
  • 3、定義事件: 使用介面定義事件名和事件數據類型,並使用 @EventGroup 註解修飾該介面:

LoginEvents.kt

@EventGroup
interface LoginEvents {

  // 事件名:login
  // 事件數據類型:UserInfo
  fun login(): UserInfo

  // 事件名:logout
  fun logout()
}
  • 4、執行註解處理器: 執行 Make ProjectRebuild Project 等多種方式都可以觸發註解處理器,處理器將根據事件定義自動生成相應的事件介面。例如,LoginEvents 對應的事件類為:

EventDefineOfLoginEvents.java

/**
 * Auto generate code, do not modify!!!
 * @see com.pengxr.sampleloginlib.events.LoginEvents 
 */
@SuppressWarnings("unchecked")
public class EventDefineOfLoginEvents implements IEventGroup {
    private EventDefineOfLoginEvents() {
    }

    public static IEvent<UserInfo> login() {
        return (IEvent<UserInfo>) (ModularEventBus.INSTANCE.createObservable("com.pengxr.sampleloginlib.events.LoginEvents$$login", UserInfo.class, false, true));
    }

    public static IEvent<Void> logout() {
        return (IEvent<Void>) (ModularEventBus.INSTANCE.createObservable("com.pengxr.sampleloginlib.events.LoginEvents$$logout", Void.class, true, false));
    }
}
  • 5、訂閱事件: 使用 EventDefineOfLoginEvents 事件類提供的靜態方法訂閱事件:

訂閱者示例

// 以生命周期感知模式訂閱事件(不需要手動註銷訂閱)
EventDefineOfLoginEvents.login().observe(this) { value: UserInfo? ->
    // Do something.
}

// 以永久模式訂閱事件(需要手動註銷訂閱)
EventDefineOfLoginEvents.logout().observeForever { _: Void? ->
    // Do something.
}
  • 6、發布事件: 使用 EventDefineOfLoginEvents 提供的靜態方法發布事件:

發布者示例

EventDefineOfLoginEvents.login().post(UserInfo("XIAOPENG"))

EventDefineOfLoginEvents.logout().post(null)
  • 7、添加混淆規則(如果使用了 minifyEnabled true):
-dontwarn com.pengxr.modular.eventbus.generated.**
-keep class com.pengxr.modular.eventbus.generated.** { *; }
-keep @com.pengxr.modular.eventbus.facade.annotation.EventGroup class * {*;} # 可選

4. 完整使用文檔

4.1 定義事件

  • 使用註解定義事件:

    • @EventGroup 註解: @EventGroup 註解用於定義事件組,修飾於 interface 介面上,在該類中定義的每個方法均視為一個事件定義;

    • @Event 註解: @Event 註解用於事件組中的事件定義,亦可省略。

模板程式如下:

com.pengxr.sample.events.MainEvents.kt

// 事件組
@EventGroup
interface MainEvents {

    // 事件
    // @Event 可以省略
    @Event
    fun open(): String
}

提示: 以上即定義了一個 MainEvents 事件組,其中包含一個 com.pengxr.sample.events.MainEvents$$open 事件且數據類型為 String 類型。

亦兼容將 @EventGroup 修飾於 class 類而非 interface 介面,但會有編譯時警告: Annotated @EventGroup on a class type [IllegalEvent], expected a interface. Is that really what you want?

錯誤示例

@EventGroup
class IllegalEvent {

    fun illegalEvent() {

    }
}
  • 使用 @Ignore 註解忽略定義: 使用 @Ignore 註解可以排除事件類或事件方法,使其不被視為事件定義。

示常式序

// 可以修飾於事件組
@Ignore
@EventGroup
interface IgnoreEvent {

    // 亦可修飾於事件
    @Ignore
    fun ignoredMethod()

    fun method()
}
  • 使用 @Deprecated 註解提示過時: 使用 @Deprecated 註解可以標記事件為過時。與 @Ignore 不同是,@Deprecated 修飾的類或方法依然是有效的事件定義。

示常式序

// 雖然過時,但依然是有效的事件定義
@Deprecated("Don't use it.")
@EventGroup
interface DeprecatedEvent {

    @Deprecated("Don't use it.")
    fun deprecatedMethod()
}
  • 定義事件數據類型: 事件方法返回值即表示事件數據類型,支援泛型(如 List<String>),支援不攜帶數據的無數據事件。以下均為合法定義:

Java 示常式序

// 事件數據類型為 String
String stringEventInJava();

// 事件數據類型為 List<String>
List<String> listEventInJava();

// 以下均視為無數據事件
void voidEventInJava1();
Void voidEventInJava2();

Kotlin 示常式序

// 事件數據類型為 String
fun stringEventInKotlin(): String

// 事件數據類型為 List<String>
fun listEventInKotlin(): List<String>

// 以下均視為無數據事件
fun voidEventInKotlin1()
fun voidEventInKotlin2(): Unit
fun voidEventInKotlin3(): Unit?
  • 定義事件數據可空性: 使用 @Nullable@NonNull 註解表示事件數據可空性,默認為可空類型。以下均為合法定義:

Java 示常式序

@NonNull
String nonNullEventInJava();

@Nullable
String nullableEventInJava();

// 默認視為 @Nullable
String eventInJava();

Kotlin 示常式序

fun nonNullEventInKotlin(): String

// 提示:Kotlin 編譯器將返回類型上的 ? 號視為 @org.jetbrains.annotations.Nullable
fun nullableEventInKotlin(): String?

以下為支援的可空性註解:

org.jetbrains.annotations.Nullable
android.annotation.Nullable
androidx.annotation.Nullable

org.jetbrains.annotations.NotNull
android.annotation.NonNull
androidx.annotation.NonNull
  • 定義自動清除事件: 支援配置在事件沒有關聯的訂閱者時自動被清除(以釋放記憶體),默認值為 false。可以使用 @EventGroup 註解或 @Event 註解進行修改,以 @Event 的取值優先。

示常式序

@EventGroup(autoClear = true)
interface MainEvents {

    @Event(autoClear = false)
    fun normalEvent(): String
    
    // 繼承 @EventGroup 中的 autoClear 取值
    fun autoClearEvent(): String
}
  • 定義事件所屬組件名: 為避免不同組件中的事件名重複,框架自動使用 "[moduleName]$$[eventName]" 作為最終的事件名。默認使用事件組的 [全限定類名] 作為 moduleName,可以使用 @EventGroup 註解進行修改。

示常式序

com.pengxr.sample.events.MainEvents.kt

@EventGroup(moduleName = "main")
interface MainEvents {

    fun open(): String
}

提示: 以上即定義了一個 MainEvents 事件組,其中包含一個 main$$open 事件且數據類型為 String 類型。

4.2 執行註解處理器

在完成事件定義後,執行 Make ProjectRebuild Project 等多種方式都可以觸發註解處理器,處理器將根據事件定義自動生成相應的事件介面。例如, MainEvents 對應的事件介面為:

com.pengxr.modular.eventbus.generated.events.com.pengxr.sample.events.EventDefineOfMainEvents.java

/**
 * Auto generate code, do not modify!!!
 * @see com.pengxr.sample.events.MainEvents 
 */
@SuppressWarnings("unchecked")
public class EventDefineOfMainEvents implements IEventGroup {
    private EventDefineOfMainEvents() {
    }

    public static IEvent<String> open() {
        return (IEvent<String>) (ModularEventBus.INSTANCE.createObservable("main$$open", String.class, false, false));
    }
}

EventDefineOfMainEvents 中的靜態方法與 MainEvent 事件組中的每個事件一一對應,直接通過靜態方法即可獲取事件實例,而不再通過手動輸入事件名字元串或事件數據類型,故可避免事件名錯誤或數據類型錯誤等問題。

所有的事件實例均是 IEvent 泛型介面的實現類,例如 open 事件屬於 IEvent<String> 類型的事件實例。發布事件和訂閱事件需要用到 IEvent 介面中定義的一系列 post 方法和 observe 方法,IEvent 介面的完整定義如下:

IEvent.kt

interface IEvent<T> {

    /**
     * 發布事件,允許在子執行緒發布
     */
    @AnyThread
    fun post(value: T?)

    /**
     * 延遲發布事件,允許在子執行緒發布
     */
    @AnyThread
    fun postDelay(value: T?, delay: Long)

    /**
     * 延遲發布事件,在準備發布前會檢查 producer 處於活躍狀態,允許在子執行緒發布
     *
     * @param producer 發布者的 LifecycleOwner
     */
    @AnyThread
    fun postDelay(value: T?, delay: Long, producer: LifecycleOwner)

    /**
     * 發布事件,允許在子執行緒發布,確保訂閱者按照發布順序接收事件
     */
    @AnyThread
    fun postOrderly(value: T?)

    /**
     * 以生命周期感知模式訂閱事件(不需要手動註銷訂閱)
     */
    @AnyThread
    fun observe(consumer: LifecycleOwner, observer: Observer<T?>)

    /**
     * 以生命周期感知模式粘性訂閱事件(不需要手動註銷訂閱)
     */
    @AnyThread
    fun observeSticky(consumer: LifecycleOwner, observer: Observer<T?>)
    
    /**
     * 以永久模式訂閱事件(需要手動註銷訂閱)
     */
    fun observeForever(observer: Observer<T?>)

    /**
     * 以永久模式粘性訂閱事件(需要手動註銷訂閱)
     *
     * @param observer Event observer.
     */
    @AnyThread
    fun observeStickyForever(observer: Observer<T?>)

    /**
     * 註銷訂閱者
     */
    @AnyThread
    fun removeObserver(observer: Observer<T?>)

    /**
     * 移除事件,關聯的訂閱者關係也會被解除
     */
    @AnyThread
    fun removeEvent()
}

4.3 訂閱事件

使用 IEvent 介面定義的一系列 observe() 介面訂閱事件,使用示例:

示常式序

// 以生命周期感知模式訂閱(不需要手動註銷訂閱)
EventDefineOfMainEvents.open().observe(this) {
    // do something.
}

// 以生命周期感知模式、且粘性模式訂閱(不需要手動註銷訂閱)
EventDefineOfMainEvents.open().observeSticky(this) {
    // do something.
}

val foreverObserver = Observer<String?> {
    // do something.
}

// 以永久模式訂閱(需要手動註銷訂閱)
EventDefineOfMainEvents.open().observeForever(foreverObserver)

// 以永久模式,且粘性模式訂閱(需要手動註銷訂閱)
EventDefineOfMainEvents.open().observeStickyForever(foreverObserver)

// 移除觀察者
EventDefineOfMainEvents.open().removeObserver(foreverObserver)

4.4 發布事件

使用 IEvent 介面定義的一系列 post() 介面發布事件,使用示例:

示常式序

// 發布事件,允許在子執行緒發布
EventDefineOfMainEvents.open().post("XIAO PENG")

// 延遲發布事件,允許在子執行緒發布
EventDefineOfMainEvents.open().postDelay("XIAO PENG", 5000)

// 延遲發布事件,在準備發布前會檢查 producer 處於活躍狀態,允許在子執行緒發布。
EventDefineOfMainEvents.open().postDelay("XIAO PENG", 5000, this)

// 發布事件,允許在子執行緒發布,確保訂閱者按照發布順序接收事件
EventDefineOfMainEvents.open().postOrderly("XIAO PENG")
  
// 移除事件
EventDefineOfMainEvents.open().removeEvent()

4.5 更多功能

  • 生成事件文檔(可選): 支援生成事件文檔,需要在 Gradle 配置中開啟:

模組級 build.gradle

// 需要生成事件文檔的模組就增加配置:
android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [
                    MODULAR_EVENTBUS_GENERATE_DOC: "enable",
                    MODULAR_EVENTBUS_MODULE_NAME : project.getName()
                ]
            }
        }
    }
}

文檔生成路徑: build/generated/source/kapt/[buildType]/com/pengxr/modular/eventbus/generated/docs/eventgroup-of-[MODULAR_EVENTBUS_MODULE_NAME].json

  • 配置(可選):
    • debug(Boolean): 調試模式開關;
    • throwNullEventException(Boolean): 非空事件發布空數據時是否拋出 NullEventException 異常,在 release 模式默認為只攔截不拋出異常,在 debug 模式默認為攔截且拋出異常;
    • setEventListener(IEventListener): 全局監聽介面。

示常式序

ModularEventBus.debug(true)
    .throwNullEventException(true)
    .setEventListener(object : IEventListener {
        override fun <T> onEventPost(eventName: String, event: BaseEvent<T>, data: T?) {
            Log.i(TAG, "onEventPost: $eventName, event = $event, data = $data")
        }
    })

5. 未來功能規劃

  • 支援跨進程 / 跨 App:LiveEventBus 框架支援跨進程 / 跨 App,未來根據使用回饋考慮實現該 Feature;
  • 支援替換內部 EventBus 工廠:ModularEventBus 已預設計事件匯流排工廠 IEventFactory,未來根據使用回饋考慮公開該 API;
  • 支援基於 Kotlin Flow 的 IEventFactory 工廠;
  • 編譯時檢查在不同 @EventGroup 中設置相同 modulaName 且相同 eventName,但事件數據類型不同的異常。

6. 共同成長

  • 歡迎提 Issue 幫助修復缺陷;
  • 歡迎提 Pull Request 增加新的 Feature,讓 ModularEventBus 變得更加強大,你的 ID 會出現在 Contributors 中;
  • 歡迎加 作者微信 與作者交流,歡迎加入交流群找到志同道合的夥伴

參考資料

我是小彭,帶你構建 Android 知識體系。技術和職場問題,請關注公眾號 [彭旭銳] 私信我提問。

Tags: