携手创作,一起生长!这是我参与「日新计划 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、Base64 编码
- 2、安全传输:加密、摘要、签名、CA 证书、防窃听、完好性、认证
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 常识系统。技能和职场问题,请重视公众号 [彭旭锐]私信我发问。