组件化做为Android客户端技术的一个重要分支,近年来一直是业界积极探索和实践的方向。美团内部各个Android开发团队也在尝试和实践不一样的组件化方案,而且在组件化通讯框架上也有不少高质量的产出。最近,咱们团队对美团零售收银和美团轻收银两款Android App进行了组件化改造。本文主要介绍咱们的组件化方案,但愿对从事Android组件化开发的同窗能有所启发。html
近年来,为何这么多团队要进行组件化实践呢?组件化究竟能给咱们的工程、代码带来什么好处?咱们认为组件化可以带来两个最大的好处:java
可能有些人会以为,提升复用性很简单,直接把须要复用的代码作成Android Module,打包AAR并上传代码仓库,那么这部分功能就能被方便地引入和使用。可是咱们以为仅仅这样是不够的,上传仓库的AAR库是否方便被复用,须要组件化的规则来约束,这样才能提升复用的便捷性。android
咱们须要经过组件化的规则把代码拆分红不一样的模块,模块要作到高内聚、低耦合。模块间也不能直接调用,这须要组件化通讯框架的支持。下降了组件间的耦合性能够带来两点直接的好处:第一,代码更便于维护;第二,下降了模块的Bug率。git
咱们的目标是要对团队的两款App(美团零售收银、美团轻收银)进行组件化重构,那么这里先简单地介绍一下这两款应用的架构。总的来讲,这两款应用的构架比较类似,主工程Module依赖Business Module,Business Module是各类业务功能的集合,Business Module依赖Service Module,Service Module依赖Platform Module,Service Module和Platform Module都对上层提供服务,有所不一样的是Platform Module提供的服务更为基础,主要包括一些工具Utils和界面Widget,而Service Module提供各类功能服务,如KNB、位置服务、网络接口调用等。这样的话,Business Module就变得很是臃肿和繁杂,各类业务模块相互调用,耦合性很强,改业务代码时容易“牵一发而动全身”,即便改一小块业务代码,可能要连带修改不少相关的地方,不只在代码层面不利于进行维护,并且对一个业务的修改很容易形成其余业务产生Bug。程序员
为了获得最适合咱们业态和构架的组件化方案,咱们调研了业界开源的一些组件化方案和公司内部其余团队的组件化方案,在此作个总结。github
咱们调研了业界一些主流的开源组件化方案。网络
号称业界首个支持渐进式组件化改造的Android组件化开源框架。不管页面跳转仍是组件间调用,都采用CC统一的组件调用方式完成。架构
获得的方案采用路由 + 接口下沉的方式,全部接口下沉到base中,组件中实现接口并在IApplicationLike中添加代码注册到Router中。app
组件间调用需指定同步实现仍是异步实现,调用组件时统一拿到RouterResponse做为返回值,同步调用的时候用RouterResponse.getData()来获取结果,异步调用获取时须要本身维护线程。框架
阿里推出的路由引擎,是一个路由框架,并非完整的组件化方案,可做为组件化架构的通讯引擎。
聚美的路由引擎,在此基础上也有聚美的组件化实践方案,基本思想是采用路由 + 接口下沉的方式实现组件化。
美团收银的组件化方案支持接口调用和消息总线两种方式,接口调用的方式须要构建CCPData,而后调用ComponentCenter.call,最后在统一的Callback中进行处理。消息总线方式也须要构建CCPData,最后调用ComponentCenter.sendEvent发送。美团收银的业务组件都打包成AAR上传至仓库,组件间存在相互依赖,这样致使mainapp引用这些组件时须要当心地exclude一些重复依赖。在咱们的组件化方案中,咱们采用了一种巧妙的方法来解决这个问题。
美团App的组件化方案采用ServiceLoader的形式,这是一种典型的接口调用组件通讯方式。用注解定义服务,获取服务时取得一个接口的List,判断这个List是否为空,若是不为空,则获取其中一个接口调用。
美团外卖团队开发的一款Android路由框架,基于组件化的设计思路。主要提供路由、ServiceLoader两大功能。以前美团技术博客也发表过一篇WMRouter的介绍:《WMRouter:美团外卖Android开源路由框架》。WMRouter提供了实现组件化的两大基础设施框架:路由和组件间接口调用。支持和文档也很充分,能够考虑做为咱们团队实现组件化的基础设施。
在前期的调研工做中,咱们发现外卖团队的WMRouter是一个不错的选择。首先,WMRouter提供了路由+ServiceLoader两大组件间通讯功能,其次,WMRouter架构清晰,扩展性比较好,而且文档和支持也比较完备。因此咱们决定了使用WMRouter做为组件化基础设施框架之一。然而,直接使用WMRouter有两个问题:
在参考了不一样的组件化方案以后,咱们采用了以下分层结构:
总体架构以下图所示:
咱们调研其余组件化方案的时候,发现不少组件方案都是把一个业务模块拆分红一个独立的业务组件,也就是拆分红一个独立的Module。而在咱们的方案中,每一个业务组件都拆分红了一个Export Module和Implement Module,为何要这样作呢?
若是采用一个业务组件一个Module的方式,若是Module A须要调用Module B提供的接口,那么Module A就须要依赖Module。同时,若是Module B须要调用Module A的接口,那么Module B就须要依赖Module A。此时就会造成一个循环依赖,这是不容许的。
也许有些读者会说,这个好解决:能够把Module A和Module B要依赖的接口放到另外一个Module中去,而后让Module A和Module B都去依赖这个Module就能够了。这确实是一个解决办法,而且有些项目组在使用这种把接口下沉的方法。
可是咱们但愿一个组件的接口,是由这个组件本身提供,而不是放在一个更加下沉的接口里面,因此咱们采用了把每一个业务组件都拆分红了一个Export Module和Implement Module。这样的话,若是Module A须要调用Module B提供的接口,同时Module B须要调用Module A的接口,只须要Module A依赖Module B Export,Module B依赖Module A Export就能够了。
在使用单Module方案的组件化方案中,这些业务组件其实不是彻底平等,有些被依赖的组件在层级上要更下沉一些。可是采用Export Module+Implement Module的方案,全部业务组件在层级上彻底平等。
每一个业务组件都划分红了Export Module+Implement Module的模式,这个时候每一个Module的功能划分也更加清晰。Export Module主要定义组件须要对外暴露的部分,主要包含:
Implement Module是组件实现的部分,主要包含:
前文提到的实现组件化基础设施框架中,咱们用外卖团队的WMRouter实现页面路由和组件间接口调用,可是却没有消息总线的基础框架,所以,咱们本身开发了一个组件化消息总线框架modular-event。
以前,咱们开发过一个基于LiveData的消息总线框架:LiveDataBus,也在美团技术博客上发表过一篇文章来介绍这个框架:《Android消息总线的演进之路:用LiveDataBus替代RxBus、EventBus》。关于消息总线的使用,老是伴随着不少争论。有些人以为消息总线很好用,有些人以为消息总线容易被滥用。
既然已经有了ServiceLoader这种组件间接口调用的框架,为何还须要消息总线这种方式呢?主要有两个理由:
基于接口调用的ServiceLoader框架的确实现了解耦,可是消息总线可以实现更完全的解耦。接口调用的方式调用方须要依赖这个接口而且知道哪一个组件实现了这个接口。消息总线方式发送者只须要发送一个消息,根本不用关心是否有人订阅这个消息,这样发送者根本不须要了解其余组件的状况,和其余组件的耦合也就越少。
基于接口的方式只能进行一对一的调用,基于消息总线的方式可以提供多对多的通讯。
总的来讲,消息总线最大的优势就是解耦,所以很适合组件化这种须要对组件间进行完全解耦的场景。然而,消息总线被不少人诟病的重要缘由,也确实是由于消息总线容易被滥用。消息总线容易被滥用通常体如今几个场景:
有时候咱们在阅读代码的过程当中,找到一个订阅消息的地方,想要看看是谁发送了这个消息,这个时候每每只能经过查找消息的方式去“溯源”。致使咱们在阅读代码,梳理逻辑的过程不太连贯,有种被割裂的感受。
消息总线在发送消息的时候通常没有强制的约束。不管是EventBus、RxBus或是LiveDataBus,在发送消息的时候既没有对消息进行检查,也没有对发送调用进行约束。这种不规范性在特定的时刻,甚至会带来灾难性的后果。好比订阅方订阅了一个名为login_success的消息,编写发送消息的是一个比较随意的程序员,没有把这个消息定义成全局变量,而是定义了一个临时变量String发送这个消息。不幸的是,他把消息名称login_success拼写成了login_seccess。这样的话,订阅方永远接收不到登陆成功的消息,并且这个错误也很难被发现。
之前咱们在使用消息总线时,喜欢把全部的消息都定义到一个公共的Java文件里面。可是组件化若是也采用这种方案的话,一旦某个组件的消息发生变更,都会去修改这个Java文件。因此咱们但愿由组件本身来定义和维护消息定义文件。
若是消息由组件定义和维护,那么有可能不一样组件定义了重名的消息,消息总线框架须要可以区分这种消息。
解决消息总线消息难以溯源和消息发送没有约束的问题。
以前的博文《Android消息总线的演进之路:用LiveDataBus替代RxBus、EventBus》详细阐述了如何基于LiveData构建消息总线。组件化消息总线框架modular-event一样会基于LiveData构建。使用LiveData构建消息总线有不少优势:
其实这个问题仍是比较好解决的,实现的方式就是采用两级HashMap的方式解决。第一级HashMap的构建以ModuleName做为Key,第二级HashMap做为Value;第二级HashMap以消息名称EventName做为Key,LiveData做为Value。查找的时候先用组件名称ModuleName在第一级HashMap中查找,若是找到则用消息名EventName在第二级HashName中查找。整个结构以下图所示:
咱们但愿消息总线框架有如下约束:
整个流程以下图所示:
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface ModuleEvents {
String module() default "";
}
复制代码
@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";
}
复制代码
咱们会在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的文章不少,这里就再也不累述。
有了自动生成的接口,就至关于有了一个壳,然而壳下面的全部逻辑,咱们经过动态代理来实现,简单介绍一下代理模式和动态代理:
在动态代理的InvocationHandler中实现查找逻辑:
消息的订阅和发送能够用链式调用的方式编码:
ModularEventBus
.get()
.of(EventsDefineOfModuleBEvents.class)
.EVENT1()
.observe(this, new Observer<TestEventBean>() {
@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"));
复制代码
本文介绍了美团行业收银研发组Android团队的组件化实践,以及业界独创强约束组件消息总线modular-event的原理和使用。咱们团队很早以前就在探索组件化改造,前期有些方案在落地的时候遇到不少困难。咱们也研究了不少开源的组件化方案,以及公司内部其余团队(美团App、美团外卖、美团收银等)的组件化方案,学习和借鉴了不少优秀的设计思想,固然也踩过很多的坑。咱们逐渐意识到:任何一种组件化方案都有其适用场景,咱们的组件化架构选择,应该更加面向业务,而不只仅是面向技术自己。
咱们的组件化改造工做远远没有结束,将来可能会在如下几个方向继续进行深刻的研究:
海亮,美团高级工程师,2017年加入美团,目前主要负责美团轻收银、美团收银零售版等App的相关业务及模块开发工做。
美团餐饮生态诚招Android高级/资深工程师和技术专家,Base北京、成都,欢迎有兴趣的同窗投递简历到chenyuxiang@meituan.com。