携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情
请点赞关注,你的支持对我意义重大。
🔥 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.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 还支持生成事件文档、空数据拦截、泛型事件、自动清除空闲事件等特性。
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>
事件;
✅ 支持拦截空数据;
✅ 支持只发布事件不携带数据的无数据事件;
✅ 支持延迟发送事件;
✅ 支持有序接收事件。
- 1、添加依赖
模块级 build.gradle
1 | gradle复制代码plugins { |
- 2、定义事件数据类型(可选): 定义事件关联的数据类型,对于只发布事件而不需要携带数据的场景,可以不定义事件类型。
UserInfo.kt
1 | kotlin复制代码data class UserInfo(val userName: String) |
- 3、定义事件: 使用接口定义事件名和事件数据类型,并使用
@EventGroup
注解修饰该接口:
LoginEvents.kt
1 | kotlin复制代码@EventGroup |
- 4、执行注解处理器: 执行
Make Project
或Rebuild Project
等多种方式都可以触发注解处理器,处理器将根据事件定义自动生成相应的事件接口。例如,LoginEvents
对应的事件类为:
EventDefineOfLoginEvents.java
1 | kotlin复制代码/** |
- 5、订阅事件: 使用
EventDefineOfLoginEvents
事件类提供的静态方法订阅事件:
订阅者示例
1 | kotlin复制代码// 以生命周期感知模式订阅事件(不需要手动注销订阅) |
- 6、发布事件: 使用
EventDefineOfLoginEvents
提供的静态方法发布事件:
发布者示例
1 | kotlin复制代码EventDefineOfLoginEvents.login().post(UserInfo("XIAOPENG")) |
- 7、添加混淆规则(如果使用了 minifyEnabled true):
1 | kotlin复制代码-dontwarn com.pengxr.modular.eventbus.generated.** |
4.1 定义事件
- 使用注解定义事件:
+ **`@EventGroup` 注解:** `@EventGroup` 注解用于定义事件组,修饰于 interface 接口上,在该类中定义的每个方法均视为一个事件定义;
+ **`@Event` 注解:** `@Event` 注解用于事件组中的事件定义,亦可省略。
模板程序如下:
com.pengxr.sample.events.MainEvents.kt
1 | kotlin复制代码// 事件组 |
提示: 以上即定义了一个
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?
错误示例
1 | kotlin复制代码@EventGroup |
- 使用
@Ignore
注解忽略定义: 使用@Ignore
注解可以排除事件类或事件方法,使其不被视为事件定义。
示例程序
1 | kotlin复制代码// 可以修饰于事件组 |
- 使用
@Deprecated
注解提示过时: 使用@Deprecated
注解可以标记事件为过时。与@Ignore
不同是,@Deprecated
修饰的类或方法依然是有效的事件定义。
示例程序
1 | less复制代码// 虽然过时,但依然是有效的事件定义 |
- 定义事件数据类型: 事件方法返回值即表示事件数据类型,支持泛型(如
List<String>
),支持不携带数据的无数据事件。以下均为合法定义:
Java 示例程序
1 | csharp复制代码// 事件数据类型为 String |
Kotlin 示例程序
1 | kotlin复制代码// 事件数据类型为 String |
- 定义事件数据可空性: 使用
@Nullable
或@NonNull
注解表示事件数据可空性,默认为可空类型。以下均为合法定义:
Java 示例程序
1 | arduino复制代码@NonNull |
Kotlin 示例程序
1 | kotlin复制代码fun nonNullEventInKotlin(): String |
以下为支持的可空性注解:
1 | kotlin复制代码org.jetbrains.annotations.Nullable |
- 定义自动清除事件: 支持配置在事件没有关联的订阅者时自动被清除(以释放内存),默认值为 false。可以使用
@EventGroup
注解或@Event
注解进行修改,以@Event
的取值优先。
示例程序
1 | kotlin复制代码@EventGroup(autoClear = true) |
- 定义事件所属组件名: 为避免不同组件中的事件名重复,框架自动使用
"[moduleName]$$[eventName]"
作为最终的事件名。默认使用事件组的[全限定类名]
作为moduleName
,可以使用@EventGroup
注解进行修改。
示例程序
com.pengxr.sample.events.MainEvents.kt
1 | kotlin复制代码@EventGroup(moduleName = "main") |
提示: 以上即定义了一个
MainEvents
事件组,其中包含一个main$$open
事件且数据类型为String
类型。
4.2 执行注解处理器
在完成事件定义后,执行 Make Project
或 Rebuild Project
等多种方式都可以触发注解处理器,处理器将根据事件定义自动生成相应的事件接口。例如, MainEvents
对应的事件接口为:
com.pengxr.modular.eventbus.generated.events.com.pengxr.sample.events.EventDefineOfMainEvents.java
1 | typescript复制代码/** |
EventDefineOfMainEvents
中的静态方法与 MainEvent
事件组中的每个事件一一对应,直接通过静态方法即可获取事件实例,而不再通过手动输入事件名字符串或事件数据类型,故可避免事件名错误或数据类型错误等问题。
所有的事件实例均是 IEvent
泛型接口的实现类,例如 open
事件属于 IEvent<String>
类型的事件实例。发布事件和订阅事件需要用到 IEvent
接口中定义的一系列 post 方法和 observe 方法,IEvent
接口的完整定义如下:
IEvent.kt
1 | less复制代码interface IEvent<T> { |
4.3 订阅事件
使用 IEvent
接口定义的一系列 observe()
接口订阅事件,使用示例:
示例程序
1 | kotlin复制代码// 以生命周期感知模式订阅(不需要手动注销订阅) |
4.4 发布事件
使用 IEvent
接口定义的一系列 post()
接口发布事件,使用示例:
示例程序
1 | kotlin复制代码// 发布事件,允许在子线程发布 |
4.5 更多功能
- 生成事件文档(可选): 支持生成事件文档,需要在 Gradle 配置中开启:
模块级 build.gradle
1 | ini复制代码// 需要生成事件文档的模块就增加配置: |
文档生成路径: 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): 全局监听接口。
示例程序
1 | kotlin复制代码ModularEventBus.debug(true) |
- 支持跨进程 / 跨 App:LiveEventBus 框架支持跨进程 / 跨 App,未来根据使用反馈考虑实现该 Feature;
- 支持替换内部 EventBus 工厂:ModularEventBus 已预设计事件总线工厂
IEventFactory
,未来根据使用反馈考虑公开该 API; - 支持基于 Kotlin Flow 的 IEventFactory 工厂;
- 编译时检查在不同
@EventGroup
中设置相同 modulaName 且相同eventName
,但事件数据类型不同的异常。
- 欢迎提 Issue 帮助修复缺陷;
- 欢迎提 Pull Request 增加新的 Feature,让 ModularEventBus 变得更加强大,你的 ID 会出现在 Contributors 中;
- 欢迎加 作者微信 与作者交流,欢迎加入交流群找到志同道合的伙伴
参考资料
- Android 消息总线的演进之路:用 LiveDataBus 替代 RxBus、EventBus —— 海亮(美团)著
- Android 组件化方案及组件消息总线 modular-event 实战 —— 海亮(美团)著
我是小彭,带你构建 Android 知识体系。技术和职场问题,请关注公众号 [彭旭锐]私信我提问。
本文转载自: 掘金