美團組件化事件匯流排方案改進:ModularEventBus
請點贊關注,你的支援對我意義重大。
🔥 Hi,我是小彭。本文已收錄到 GitHub · AndroidFamily 中。這裡有 Android 進階成長知識體系,有志同道合的朋友,關注公眾號 [彭旭銳] 帶你建立核心競爭力。
前言
大家好,我是小彭。2 年前,我們在 為了組件化改造學習十幾家大廠的技術部落格 這篇文章里收集過各大廠的組件化方案。其中,有美團收銀團隊分享的組件化匯流排框架 modular-event
讓我們印象深刻。然而,美團並未將該框架開源,我們只能望梅止渴。
在學習和借鑒美團 modular-event
方案中很多優秀的設計思想後,我亦發現方案中依然存在不一致風險和不足,故我決定對方案進行改進並向社區開源。項目主頁為 Github · ModularEventBus,演示 Demo 可直接下載:
Demo apk。
歡迎提 Issue 幫助修復缺陷,歡迎提 Pull Request 增加新的 Feature,有用請點贊給 Star,給小彭一點創作的動力,謝謝。
這篇文章是 組件化系列文章第 5 篇,相關 Android 工程化專欄完整文章列表:
一、Gradle 基礎:
- 1、Gradle 基礎 :Wrapper、Groovy、生命周期、Project、Task、增量
- 2、Gradle 插件:Plugin、Extension 擴展、NamedDomainObjectContainer、調試
- 3、Gradle 依賴管理
- 4、Maven 發布:SHAPSHOT 快照、uploadArchives、Nexus、AAR
- 5、Gradle 插件案例:EasyPrivacy、so 文件適配 64 位架構、ABI
二、AGP 插件:
- 1、AGP 構建過程
- 2、AGP 常用配置項:Manifest、BuildConfig、buildTypes、殼工程、環境切換
- 3、APG Transform:AOP、TransformTask、增量、位元組碼、Dex
- 4、AGP 程式碼混淆:ProGuard、R8、Optimize、Keep、組件化
- 5、APK 簽名:認證、完整性、v1、v2、v3、Zip、Wallet
- 6、AGP 案例:多渠道打包
三、組件化開發:
- 1、方案積累:有贊、蘑菇街、得到、攜程、支付寶、手淘、愛奇藝、微信、美團
- 2、組件化架構基礎
- 3、ARouter 源碼分析
- 4、組件化案例:通用方案
- 5、組件化案例:組件化事件匯流排框架(本文)
- 6、組件化案例:組件化 Key-Value 框架
四、AOP 面向切面編程:
- 1、AOP 基礎
- 2、Java 註解
- 3、Java 註解處理器:APT、javac
- 4、Java 動態代理:代理模式、Proxy、位元組碼
- 5、Java ServiceLoader:服務發現、SPI、META-INF
- 6、AspectJ 框架:Transform
- 7、Javassist 框架
- 8、ASM 框架
- 9、AspectJ 案例:限制按鈕點擊抖動
五、相關電腦基礎
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 Project
或Rebuild 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 Project
或 Rebuild 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 消息匯流排的演進之路:用 LiveDataBus 替代 RxBus、EventBus —— 海亮(美團)著
- Android 組件化方案及組件消息匯流排 modular-event 實戰 —— 海亮(美團)著
我是小彭,帶你構建 Android 知識體系。技術和職場問題,請關注公眾號 [彭旭銳] 私信我提問。