当前位置:   article > 正文

Android组件化方案及组件消息总线modular-event实战(2)

Android组件化方案及组件消息总线modular-event实战(2)

组件结构

2. 业务组件完全平等

在使用单Module方案的组件化方案中,这些业务组件其实不是完全平等,有些被依赖的组件在层级上要更下沉一些。但是采用Export Module+Implement Module的方案,所有业务组件在层级上完全平等。

3. 功能划分更加清晰

每个业务组件都划分成了Export Module+Implement Module的模式,这个时候每个Module的功能划分也更加清晰。Export Module主要定义组件需要对外暴露的部分,主要包含:

  • 对外暴露的接口,这些接口用WMRouter的ServiceLoader进行调用。
  • 对外暴露的事件,这些事件利用消息总线框架modular-event进行订阅和分发。
  • 组件的Router Path,组件化之前的工程虽然也使用了Router框架,但是所有Router Path都是定义在了一个下沉Module的公有Class中。这样导致的问题是,无论哪个模块添加/删除页面,或是修改路由,都需要去修改这个公有的Class。设想如果组件化拆分之后,某个组件新增了页面,还要去一个外部的Java文件中新增路由,这显然难以接受,也不符合组件化内聚的目标。因此,我们把每个组件的Router Path放在组件的Export Module中,既可以暴露给其他组件,也可以做到每个组件管理自己的Router Path,不会出现所有组件去修改一个Java文件的窘境。

Implement Module是组件实现的部分,主要包含:

  • 页面相关的Activity、Fragment,并且用WMRouter的注解定义路由。
  • 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构建消息总线有很多优点:

  1. 使用LiveData构建消息总线具有生命周期感知能力,使用者不需要调用反注册,相比EventBus和RxBus使用更为方便,并且没有内存泄漏风险。
  2. 使用普通消息总线,如果回调的时候Activity处于Stop状态,这个时候进行弹Dialog一类的操作就会引起崩溃。使用LiveData构建消息总线完全没有这个风险。

组件消息总线modular-event的实现

解决不同组件定义了重名消息的问题

其实这个问题还是比较好解决的,实现的方式就是采用两级HashMap的方式解决。第一级HashMap的构建以ModuleName作为Key,第二级HashMap作为Value;第二级HashMap以消息名称EventName作为Key,LiveData作为Value。查找的时候先用组件名称ModuleName在第一级HashMap中查找,如果找到则用消息名EventName在第二级HashName中查找。整个结构如下图所示:

消息总线结构

对消息总线的约束

我们希望消息总线框架有以下约束:

  1. 只能订阅和发送在组件中预定义的消息。换句话说,使用者不能发送和订阅临时消息。
  2. 消息的类型需要在定义的时候指定。
  3. 定义消息的时候需要指定属于哪个组件。

如何实现这些约束

  1. 在消息定义文件上使用注解,定义消息的类型和消息所属Module。
  2. 定义注解处理器,在编译期间收集消息的相关信息。
  3. 在编译器根据消息的信息生成调用时需要的interface,用接口约束消息发送和订阅。
  4. 运行时构建基于两级HashMap的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 {
String module() default “”;
}

  • @EventType:消息类型

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface EventType {
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 {

//不指定消息类型,那么消息的类型默认为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 {
com.sankuai.erp.modularevent.Observable<java.lang.Object> EVENT1();

com.sankuai.erp.modularevent.Observable<com.meituan.jeremy.module_b_export.TestEventBean> EVENT2(
);

com.sankuai.erp.modularevent.Observable<java.lang.String> EVENT3();
}

关于接口类的自动生成,我们采用了square/javapoet来实现,网上介绍JavaPoet的文章很多,这里就不再累述。

使用动态代理实现运行时调用

有了自动生成的接口,就相当于有了一个壳,然而壳下面的所有逻辑,我们通过动态代理来实现,简单介绍一下代理模式和动态代理:

  • 代理模式: 给某个对象提供一个代理对象,并由代理对象控制对于原对象的访问,即客户不直接操控原对象,而是通过代理对象间接地操控原对象。
  • 动态代理: 代理类是在运行时生成的。也就是说Java编译完之后并没有实际的class文件,而是在运行时动态生成的类字节码,并加载到JVM中。

在动态代理的InvocationHandler中实现查找逻辑:

  1. 根据interface的typename得到ModuleName。
  2. 调用的方法的methodname即为消息名。
  3. 根据ModuleName和消息名找到相应的LiveData。
  4. 完成后续订阅消息或者发送消息的流程。

消息的订阅和发送可以用链式调用的方式编码:

  • 订阅消息

ModularEventBus
.get()
.of(EventsDefineOfModuleBEvents.class)
.EVENT1()
.observe(this, new Observer() {
@Override
public void onChanged(@Nullable TestEventBean testEventBean) {
Toast.makeText(MainActivity.this, "MainActivity收到自定义消息: " + testEventBean.getMsg(),
Toast.LENGTH_SHORT).show();
}
});

  • 发送消息

ModularEventBus
.get()
.of(EventsDefineOfModuleBEvents.class)
.EVENT1()
.setValue(new TestEventBean(“aa”));

订阅和发送的模式
  • 订阅消息的模式
  1. observe:生命周期感知,onDestroy的时候自动取消订阅。
  2. observeSticky:生命周期感知,onDestroy的时候自动取消订阅,Sticky模式。
  3. observeForever:需要手动取消订阅。
  4. observeStickyForever:需要手动取消订阅,Sticky模式。
  • 发送消息的模式
  1. setValue:主线程调用。
  2. postValue:后台线程调用。

总结

本文介绍了美团行业收银研发组Android团队的组件化实践,以及强约束组件消息总线modular-event的原理和使用。我们团队很早之前就在探索组件化改造,前期有些方案在落地的时候遇到很多困难。我们也研究了很多开源的组件化方案,以及公司内部其他团队(美团App、美团外卖、美团收银等)的组件化方案,学习和借鉴了很多优秀的设计思想,当然也踩过不少的坑。我们逐渐意识到:任何一种组件化方案都有其适用场景,我们的组件化架构选择,应该更加面向业务,而不仅仅是面向技术本身。

后期工作展望

我们的组件化改造工作远远没有结束,未来可能会在以下几个方向继续进行深入的研究:

  1. 组件管理:组件化改造之后,每个组件是个独立的工程,组件也会迭代开发,如何对这些组件进行版本化管理。
  2. 组件重用:现在看起来对这些组件的重用是很方便的,只需要引入组件的库即可,但是如果一个新的项目到来,需求有些变化,我们应该怎样最大限度的重用这些组件。
  3. CI集成:如何更好的与CI集成。
  4. 集成到脚手架:集成到脚手架,让新的项目从一开始就以组件化的模式进行开发。

文末

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

结语

网上高级工程师面试相关文章鱼龙混杂,要么一堆内容,要么内容质量太浅, 鉴于此我整理了上述安卓开发高级工程师面试题以及答案。希望帮助大家顺利进阶为高级工程师。
目前我就职于某大厂安卓高级工程师职位,在当下大环境下也想为安卓工程师出一份力,通过我的技术经验整理了面试经常问的题,答案部分是一篇文章或者几篇文章,都是我认真看过并且觉得不错才整理出来。

大家知道高级工程师不会像刚入门那样被问的问题一句话两句话就能表述清楚,所以我通过过滤好文章来帮助大家理解。

1307页字节跳动Android面试真题解析火爆全网,完整版开放下载

现在都说互联网寒冬,其实只要自身技术能力够强,咱们就不怕!我这边专门针对Android开发工程师整理了一套【Android进阶学习视频】、【全套Android面试秘籍】、【Android知识点PDF】。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

好文章来帮助大家理解。

[外链图片转存中…(img-XUwrvloY-1713275944935)]

现在都说互联网寒冬,其实只要自身技术能力够强,咱们就不怕!我这边专门针对Android开发工程师整理了一套【Android进阶学习视频】、【全套Android面试秘籍】、【Android知识点PDF】。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/508100
推荐阅读
相关标签
  

闽ICP备14008679号