- Export Module中对外暴露的接口的实现。
- 其他的业务逻辑。
组件功能划分
组件功能划分
组件化消息总线框架modular-event
前文提到的实现组件化基础设施框架中,我们用外卖团队的WMRouter实现页面路由和组件间接口调用,但是却没有消息总线的基础框架,因此,我们自己开发了一个组件化消息总线框架modular-event。
为什么需要消息总线框架
既然已经有了ServiceLoader这种组件间接口调用的框架,为什么还需要消息总线这种方式呢?主要有两个理由。
1. 更进一步的解耦
基于接口调用的ServiceLoader框架的确实现了解耦,但是消息总线能够实现更彻底的解耦。接口调用的方式调用方需要依赖这个接口并且知道哪个组件实现了这个接口。消息总线方式发送者只需要发送一个消息,根本不用关心是否有人订阅这个消息,这样发送者根本不需要了解其他组件的情况,和其他组件的耦合也就越少。
2. 多对多的通信
基于接口的方式只能进行一对一的调用,基于消息总线的方式能够提供多对多的通信。
消息总线的优点和缺点
总的来说,消息总线最大的优点就是解耦,因此很适合组件化这种需要对组件间进行彻底解耦的场景。然而,消息总线被很多人诟病的重要原因,也确实是因为消息总线容易被滥用。消息总线容易被滥用一般体现在几个场景:
1. 消息难以溯源
有时候我们在阅读代码的过程中,找到一个订阅消息的地方,想要看看是谁发送了这个消息,这个时候往往只能通过查找消息的方式去“溯源”。导致我们在阅读代码,梳理逻辑的过程不太连贯,有种被割裂的感觉。
2. 消息发送比较随意,没有强制的约束
消息总线在发送消息的时候一般没有强制的约束。无论是EventBus、RxBus或是LiveDataBus,在发送消息的时候既没有对消息进行检查,也没有对发送调用进行约束。这种不规范性在特定的时刻,甚至会带来灾难性的后果。比如订阅方订阅了一个名为login_success的消息,编写发送消息的是一个比较随意的程序员,没有把这个消息定义成全局变量,而是定义了一个临时变量String发送这个消息。不幸的是,他把消息名称login_success拼写成了login_seccess。这样的话,订阅方永远接收不到登录成功的消息,而且这个错误也很难被发现。
组件化消息总线的设计目标
1. 消息由组件自己定义
以前我们在使用消息总线时,喜欢把所有的消息都定义到一个公共的Java文件里面。但是组件化如果也采用这种方案的话,一旦某个组件的消息发生变动,都会去修改这个Java文件。所以我们希望由组件自己来定义和维护消息定义文件。
2. 区分不同组件定义的同名消息
如果消息由组件定义和维护,那么有可能不同组件定义了重名的消息,消息总线框架需要能够区分这种消息。
3. 解决前文提到的消息总线的缺点
解决消息总线消息难以溯源和消息发送没有约束的问题。
基于LiveData的消息总线
组件化消息总线框架modular-event基于LiveData构建,使用LiveData构建消息总线有很多优点:
- 使用LiveData构建消息总线具有生命周期感知能力,使用者不需要调用反注册,相比EventBus和RxBus使用更为方便,并且没有内存泄漏风险。
- 使用普通消息总线,如果回调的时候Activity处于Stop状态,这个时候进行弹Dialog一类的操作就会引起崩溃。使用LiveData构建消息总线完全没有这个风险。
组件消息总线modular-event的实现
解决不同组件定义了重名消息的问题
其实这个问题还是比较好解决的,实现的方式就是采用两级HashMap的方式解决。第一级HashMap的构建以ModuleName作为Key,第二级HashMap作为Value;第二级HashMap以消息名称EventName作为Key,LiveData作为Value。查找的时候先用组件名称ModuleName在第一级HashMap中查找,如果找到则用消息名EventName在第二级HashName中查找。整个结构如下图所示:
消息总线结构
对消息总线的约束
我们希望消息总线框架有以下约束:
- 只能订阅和发送在组件中预定义的消息。换句话说,使用者不能发送和订阅临时消息。
- 消息的类型需要在定义的时候指定。
- 定义消息的时候需要指定属于哪个组件。
如何实现这些约束
- 在消息定义文件上使用注解,定义消息的类型和消息所属Module。
- 定义注解处理器,在编译期间收集消息的相关信息。
- 在编译器根据消息的信息生成调用时需要的interface,用接口约束消息发送和订阅。
- 运行时构建基于两级HashM
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
ap的LiveData存储结构。
5. 运行时采用interface+动态代理的方式实现真正的消息订阅和发送。
整个流程如下图所示:
消息总线modular-event的结构
- modular-event-base:定义Anotation及其他基本类型
- modular-event-core:modular-event核心实现
- modular-event-compiler:注解处理器
- modular-event-plugin:Gradle Plugin
Anotation
- @ModuleEvents:消息定义
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface ModuleEvents {undefined
String module() default “”;
}
- @EventType:消息类型
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface EventType {undefined
Class value();
}
消息定义
通过@ModuleEvents注解一个定义消息的Java类,如果@ModuleEvents指定了属性module,那么这个module的值就是这个消息所属的Module,如果没有指定属性module,则会把定义消息的Java类所在的包的包名作为消息所属的Module。
在这个消息定义java类中定义的消息都是public static final String类型。可以通过@EventType指定消息的类型,@EventType支持java原生类型或自定义类型,如果没有用@EventType指定消息类型,那么消息的类型默认为Object,下面是一个消息定义的示例:
//可以指定module,若不指定,则使用包名作为module名
@ModuleEvents()
public class DemoEvents {undefined
//不指定消息类型,那么消息的类型默认为Object
public static final String EVENT1 = “event1”;
//指定消息类型为自定义Bean
@EventType(TestEventBean.class)
public static final String EVENT2 = “event2”;
//指定消息类型为java原生类型
@EventType(String.class)
public static final String EVENT3 = “event3”;
}
interface自动生成
我们会在modular-event-compiler中处理这些注解,一个定义消息的Java类会生成一个接口,这个接口的命名是EventsDefineOf+消息定义类名,例如消息定义类的类名为DemoEvents,自动生成的接口就是EventsDefineOfDemoEvents。消息定义类中定义的每一个消息,都会转化成接口中的一个方法。使用者只能通过这些自动生成的接口使用消息总线。我们用这种巧妙的方式实现了对消息总线的约束。前文提到的那个消息定义示例DemoEvents.java会生成一个如下的接口类:
package com.sankuai.erp.modularevent.generated.com.meituan.jeremy.module_b_export;
public interface EventsDefineOfDemoEvents extends com.sankuai.erp.modularevent.base.IEventsDefine {undefined
com.sankuai.erp.modularevent.Observable
com.sankuai.erp.modularevent.Observable
);
com.sankuai.erp.modularevent.Observable
}
关于接口类的自动生成,我们采用了square/javapoet来实现,网上介绍JavaPoet的文章很多,这里就不再累述。
使用动态代理实现运行时调用
有了自动生成的接口,就相当于有了一个壳,然而壳下面的所有逻辑,我们通过动态代理来实现,简单介绍一下代理模式和动态代理:
- 代理模式: 给某个对象提供一个代理对象,并由代理对象控制对于原对象的访问,即客户不直接操控原对象,而是通过代理对象间接地操控原对象。
- 动态代理: 代理类是在运行时生成的。也就是说Java编译完之后并没有实际的class文件,而是在运行时动态生成的类字节码,并加载到JVM中。
在动态代理的InvocationHandler中实现查找逻辑:
- 根据interface的typename得到ModuleName。
- 调用的方法的methodname即为消息名。
- 根据ModuleName和消息名找到相应的LiveData。
- 完成后续订阅消息或者发送消息的流程。
消息的订阅和发送可以用链式调用的方式编码:
- 订阅消息
ModularEventBus
.get()
.of(EventsDefineOfModuleBEvents.class)
.EVENT1()
.observe(this, new Observer() {undefined
@Override
public void onChanged(@Nullable TestEventBean testEventBean) {undefined
Toast.makeText(MainActivity.this, "MainActivity收到自定义消息: " + testEventBean.getMsg(),
Toast.LENGTH_SHORT).show();
}
});
- 发送消息
ModularEventBus
.get()
.of(EventsDefineOfModuleBEvents.class)
.EVENT1()
.setValue(new TestEventBean(“aa”));
订阅和发送的模式
- 订阅消息的模式
- observe:生命周期感知,onDestroy的时候自动取消订阅。
- observeSticky:生命周期感知,onDestroy的时候自动取消订阅,Sticky模式。
- observeForever:需要手动取消订阅。
- observeStickyForever:需要手动取消订阅,Sticky模式。
- 发送消息的模式
- setValue:主线程调用。
- postValue:后台线程调用。
总结
本文介绍了美团行业收银研发组Android团队的组件化实践,以及强约束组件消息总线modular-event的原理和使用。我们团队很早之前就在探索组件化改造,前期有些方案在落地的时候遇到很多困难。我们也研究了很多开源的组件化方案,以及公司内部其他团队(美团App、美团外卖、美团收银等)的组件化方案,学习和借鉴了很多优秀的设计思想,当然也踩过不少的坑。我们逐渐意识到:任何一种组件化方案都有其适用场景,我们的组件化架构选择,应该更加面向业务,而不仅仅是面向技术本身。
研发组Android团队的组件化实践,以及强约束组件消息总线modular-event的原理和使用。我们团队很早之前就在探索组件化改造,前期有些方案在落地的时候遇到很多困难。我们也研究了很多开源的组件化方案,以及公司内部其他团队(美团App、美团外卖、美团收银等)的组件化方案,学习和借鉴了很多优秀的设计思想,当然也踩过不少的坑。我们逐渐意识到:任何一种组件化方案都有其适用场景,我们的组件化架构选择,应该更加面向业务,而不仅仅是面向技术本身。