美團組件化事件總線方案改進:ModularEventBus

前言

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

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

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


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

一、Gradle 基礎:

二、AGP 插件:

三、組件化開發(fā):

四、AOP 面向切面編程:

五、相關計算機基礎


1. 認識事件總線

1.1 事件總線的優(yōu)點

事件總線框架最大的優(yōu)點是 ”解耦“,即事件發(fā)布者與事件訂閱者的解耦,事件的發(fā)布者不需要關心是否有人訂閱該事件,也不需要關心是誰訂閱該事件,代碼耦合度較低。因此,事件總線框架更適合作為全局的事件通信方案,或者組件間通信的輔助方案。

1.2 事件總線的缺點

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

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

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

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

  • 4、事件命名重復: 由于框架對事件命名沒有強制約束,不同組件有可能定義重名的事件,產(chǎn)生邏輯錯誤。如果重名的事件還使用了不同的數(shù)據(jù)類型,還會出現(xiàn)類型轉換錯誤,存在隱患;

  • 5、事件命名疏忽: 與 ”事件命名重復“ 類似,由于框架對事件命名沒有檢查,有可能出現(xiàn)開發(fā)者復制粘貼后忘記修改事件變量值的問題,或者變量值拼寫錯誤(例如 login_success 拼寫為 login_succese),那么訂閱方將永遠收不到事件。

1.3 ModularEventBus 的解決方案

ModularEventBus 組件化事件總線框架的優(yōu)點是: 在保持發(fā)布者與訂閱者的解耦的優(yōu)勢下,解決上述事件總線框架中存在的通病。 具體通過以下 5 個手段實現(xiàn):

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

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

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

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

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

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

  • modular-event 使用靜態(tài)常量定義事件,為什么 ModularEventBus 用接口定義事件?

    美團 modular-event 使用常量引入了重復信息,存在不一致風險。例如開發(fā)者復制一行常量后,只修改常量名但忘記修改值,這種錯誤往往很難被發(fā)現(xiàn)。而 ModularEventBus 使用方法名作為事件名,方法返回值作為事件數(shù)據(jù)類型,不會引入重復信息且更加簡潔。

  • modular-event 使用動態(tài)代理,為什么 ModularEventBus 不需要?

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

  • 更多特性支持:

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


2. ModularEventBus 能做什么?

ModularEventBus 是一款幫助 Android App 解決事件總線濫用問題的框架,亦可作為組件化基礎設施。 其解決方案是通過注解定義事件,由編譯時 APT 注解處理器進行合法性檢查和自動生成事件接口,以實現(xiàn)對事件定義、發(fā)布和訂閱的強約束。

2.1 常見事件總線框架對比

以下從多個維度對比常見的事件總線框架( ? 良好支持、?? 支持、? 不支持):

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

2.2 ModularEventBus 特性一覽

1、事件強約束

? 支持零配置快速使用;

? 支持 APT 注解處理器自動生成事件接口類;

? 支持編譯時合法性校驗和警告提示;

? 支持生成事件文檔;

? 支持增量編譯;

2、Lifecycle 生命周期感知

? 內置基于 LiveData 的 LiveDataBus;

? 支持自動取消訂閱,避免內存泄漏;

? 支持安全地發(fā)送事件與接收事件,避免產(chǎn)生空指針異?;虿槐匾男阅軗p耗;

? 支持永久訂閱事件;

? 支持自動清除沒有關聯(lián)訂閱者的空閑 LiveData 以釋放內存;

3、更多特性支持

? 支持 Java / Kotlin;

? 支持 AndroidX;

? 支持訂閱 Sticky 粘性事件,支持移除事件;

? 支持 Generic 泛型事件,如 List<String> 事件;

? 支持攔截空數(shù)據(jù);

? 支持只發(fā)布事件不攜帶數(shù)據(jù)的無數(shù)據(jù)事件;

? 支持延遲發(fā)送事件;

? 支持有序接收事件。


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、定義事件數(shù)據(jù)類型(可選): 定義事件關聯(lián)的數(shù)據(jù)類型,對于只發(fā)布事件而不需要攜帶數(shù)據(jù)的場景,可以不定義事件類型。

UserInfo.kt

data class UserInfo(val userName: String)
  • 3、定義事件: 使用接口定義事件名和事件數(shù)據(jù)類型,并使用 @EventGroup 注解修飾該接口:

LoginEvents.kt

@EventGroup
interface LoginEvents {

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

  // 事件名:logout
  fun logout()
}
  • 4、執(zhí)行注解處理器: 執(zhí)行 Make ProjectRebuild Project 等多種方式都可以觸發(fā)注解處理器,處理器將根據(jù)事件定義自動生成相應的事件接口。例如,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 事件類提供的靜態(tài)方法訂閱事件:

訂閱者示例

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

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

發(fā)布者示例

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

EventDefineOfLoginEvents.logout().post(null)
  • 7、添加混淆規(guī)則(如果使用了 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 事件且數(shù)據(jù)類型為 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()
}
  • 定義事件數(shù)據(jù)類型: 事件方法返回值即表示事件數(shù)據(jù)類型,支持泛型(如 List<String>),支持不攜帶數(shù)據(jù)的無數(shù)據(jù)事件。以下均為合法定義:

Java 示例程序

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

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

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

Kotlin 示例程序

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

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

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

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
  • 定義自動清除事件: 支持配置在事件沒有關聯(lián)的訂閱者時自動被清除(以釋放內存),默認值為 false??梢允褂?@EventGroup 注解或 @Event 注解進行修改,以 @Event 的取值優(yōu)先。

示例程序

@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 事件且數(shù)據(jù)類型為 String 類型。

4.2 執(zhí)行注解處理器

在完成事件定義后,執(zhí)行 Make ProjectRebuild Project 等多種方式都可以觸發(fā)注解處理器,處理器將根據(jù)事件定義自動生成相應的事件接口。例如, 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 中的靜態(tài)方法與 MainEvent 事件組中的每個事件一一對應,直接通過靜態(tài)方法即可獲取事件實例,而不再通過手動輸入事件名字符串或事件數(shù)據(jù)類型,故可避免事件名錯誤或數(shù)據(jù)類型錯誤等問題。

所有的事件實例均是 IEvent 泛型接口的實現(xiàn)類,例如 open 事件屬于 IEvent<String> 類型的事件實例。發(fā)布事件和訂閱事件需要用到 IEvent 接口中定義的一系列 post 方法和 observe 方法,IEvent 接口的完整定義如下:

IEvent.kt

interface IEvent<T> {

    /**
     * 發(fā)布事件,允許在子線程發(fā)布
     */
    @AnyThread
    fun post(value: T?)

    /**
     * 延遲發(fā)布事件,允許在子線程發(fā)布
     */
    @AnyThread
    fun postDelay(value: T?, delay: Long)

    /**
     * 延遲發(fā)布事件,在準備發(fā)布前會檢查 producer 處于活躍狀態(tài),允許在子線程發(fā)布
     *
     * @param producer 發(fā)布者的 LifecycleOwner
     */
    @AnyThread
    fun postDelay(value: T?, delay: Long, producer: LifecycleOwner)

    /**
     * 發(fā)布事件,允許在子線程發(fā)布,確保訂閱者按照發(fā)布順序接收事件
     */
    @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?>)

    /**
     * 移除事件,關聯(lián)的訂閱者關系也會被解除
     */
    @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 發(fā)布事件

使用 IEvent 接口定義的一系列 post() 接口發(fā)布事件,使用示例:

示例程序

// 發(fā)布事件,允許在子線程發(fā)布
EventDefineOfMainEvents.open().post("XIAO PENG")

// 延遲發(fā)布事件,允許在子線程發(fā)布
EventDefineOfMainEvents.open().postDelay("XIAO PENG", 5000)

// 延遲發(fā)布事件,在準備發(fā)布前會檢查 producer 處于活躍狀態(tài),允許在子線程發(fā)布。
EventDefineOfMainEvents.open().postDelay("XIAO PENG", 5000, this)

// 發(fā)布事件,允許在子線程發(fā)布,確保訂閱者按照發(fā)布順序接收事件
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): 非空事件發(fā)布空數(shù)據(jù)時是否拋出 NullEventException 異常,在 release 模式默認為只攔截不拋出異常,在 debug 模式默認為攔截且拋出異常;
    • setEventListener(IEventListener): 全局監(jiān)聽接口。

示例程序

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. 未來功能規(guī)劃

  • 支持跨進程 / 跨 App:LiveEventBus 框架支持跨進程 / 跨 App,未來根據(jù)使用反饋考慮實現(xiàn)該 Feature;
  • 支持替換內部 EventBus 工廠:ModularEventBus 已預設計事件總線工廠 IEventFactory,未來根據(jù)使用反饋考慮公開該 API;
  • 支持基于 Kotlin Flow 的 IEventFactory 工廠;
  • 編譯時檢查在不同 @EventGroup 中設置相同 modulaName 且相同 eventName,但事件數(shù)據(jù)類型不同的異常。

6. 共同成長

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

參考資料

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容