what?什么是组件化?
简介
客户端(架构)yanlu的老文章
Android组件化开发实践和案例分享(方案1)
Android 组件化最佳实践(方案1)
Android组件化开发思想与实践(方案1)
回归初心:极简 Android 组件化方案(方案2:推荐)
可以阅读以上三篇文章,了解组件化的基础概念。
实现组件化目前有2套主要的方案:(1)改造module,支持library和application两种运行模式,开发过程中进行手动切换。(2)为每个module新增一个专门用于独立运行的AppModule,module无需改动,跑整包或组件包,运行不同的AppModule即可;基于开闭原则,及低侵入性,操作便捷性等优点,推荐使用方案2
演示
SVID_20200929_102256_1.mp4
将多鹿app中test相关的代码抽取成独立module后,即可独立运行。
why?为什么需要组件化?组件化有什么优点?
组件独立打包运行,提升开发效率(提升10倍左右编译效率):
这是我们平常的开发模式,当我们有新业务时,会创建一个新的module A,或者直接在module app中开发,每次修改一行Java代码,进行的增量编译时间大概是50秒(自己电脑亲测)。为什么这么慢?因为这时候打的是整包,每次编译都会触发app里所有代码的javac,dexMerge,packageApk等,然后去编译一个完整的多鹿apk。假设我只是修改Module A的一行代码,需要去编译APP的所有代码吗?如何只编译Module A中我修改的代码呢?答案是:跳过APP,只打Module A的组件包。
如图:为Module A单独创建一个Module A App(这是一个Application),包含了Module A运行所需要的必要依赖library、biz、api和其它三方SDK,如:Arouter,Retrofit等(不需要像APP那样依赖众多的三方SDK)。此时单独运行Module A App,修改一行Java代码的增量编译时间仅仅只要5秒左右(原因可想而知:1.摆脱了APP,此时moduleA中的业务代码极少;2.ModuleA移除了不必要的三方SDK和依赖,只有少量,必要的SDK)。当然,如果这个module A要依赖更多的三方SDK,编译速度也会有所下降,所以要尽量保持组件module的最小化,才能实现编译效率的最大提升。开发阶段,通过Module A APP运行组件包,调试代码;集成阶段,通过Module APP运行整包,完善与其他组件的联调,Module A则还是一个独立的Library,除了业务代码,无需其他任何改动。
动态化/插件化
组件化的最终目的是为了实现插件化,在运行期间动态的替换,更新组件,实现业务功能的动态化。
高内聚,低耦合
如果不进行组件化拆分,随着APP代码的增多,编译会越来越慢
减少代码耦合,提高代码复用率
module拆分出来后,可以更方便的进行单元测试,A/B Test等
组件微服务化,更利于跨部门协作。大团队中,不同部门/团队负责不同组件,发版前集成并编译整包。小团队中,如果可以将代码打散成一个个module,每个人负责1-2个组件,各个模块之间根据api层进行解耦,就可以最大限度的减少冲突,实现更高效的并行开发。
编辑APK时,可以根据BC端,依赖不同的module,只将BC端中用到的代码打进apk,可以优化APK大小。
基于多鹿/多鹿老师APP,可以拆出哪些组件?
以业务为导向进行拆分的module,称谓模块,可实现C/B端的复用。例如:首页模块,动态列表模块,消息模块,我的模块,动态发布模块,宝宝模块,班级模块,学校模块,直播模块,成长册模块,小目标模块...等等
以技术为导向进行拆分的module,称谓组件,可实现不同模块之间的复用。例如:登录组件,播放器组件,支付组件,分享组件,IM组件,短视频处理组件...等等
总结
1.能立马见到的收益:组件独立运行,提升10倍编译效率。
2.长期收益:代码架构的健壮性,复用性,和可扩展性。
how?如何创建出一个可以独立运行起来的组件?
module接口层
如图,这是我们将要采用的方案2的组件开发架构。但是还存在一个问题,就是不同module的循环依赖。
假设moduleA可能依赖moduleB的某些方法,同时moduleB也会依赖moduleA某些方法的情况,这样会产生循环依赖,是不被允许的。解决方案:为A和B分别创建Module A-I,Module B-I接口层,将A、B对外开放的model,interface,或是event都放在接口层,A、B分别实现A-I和B-I的接口协议,彼此module之间不依赖,且不应该知道彼此的实现细节,如下图。
所以假设现在有个module A,要实现组件化拆分,就需要为这个module分别创建module A-I(对外暴露接口),module A-App(独立运行组件)两个module,APP应该只依赖module A-I里面的接口,不该使用Module A的实现类。
多鹿APP基于组件化拆分的示例模板
标准的组件化module模板
module_template_api
ModuleTemplateApiEvent:组件对外暴露的event,尽量把和这个组件相关的event放在module_api中,而不是api,保证内聚,app或者其他module可以依赖这个组件的module_api。
public class ModuleTemplateApiEvent {
public String status;
public ModuleTemplateApiEvent(String status) {
this.status = status;
}
}
ModuleTemplateApiModel:组件对外暴露的model。
public class ModuleTemplateApiModel extends BaseReqModel {
public String id;
public String name;
}
ModuleTemplateApiScheme:组件对外暴露的scheme协议。
/**
* 组件scheme协议
* @author listen
*/
public class ModuleTemplateApiScheme {
public static final String MODULE_MAIN = "/module_template_api/main";
public static final String MODULE_SERVICE_NAME = "/module_template_api/service";
}
ModuleTemplateApiService:组件对外暴露的接口协议(这里使用Arouter Service实现)。
public interface ModuleTemplateApiService extends IProvider {
/**
* 获取moduleName
* @return moduleName
*/
String getModuleName();
}
module_template
Constanst:常量
ModuleTemplateActivity:组件中示例代码,如网络请求,发送消息,跳转H5,Flutter等
ModuleTemplateApplication:组件的内部私有的初始化逻辑,假设当前是直播组件,则这里就是zegoSDK的初始化逻辑,保证直播相关逻辑内聚在这个module中。
/**
* 当前业务module内部私有的初始化逻辑
*/
public class ModuleTemplateApplication extends Application {
private static final String TAG = "ModuleTemplateApplication";
private static Context sApplication;
public static Context getInstance() {
return sApplication;
}
@SuppressLint("LongLogTag")
public ModuleTemplateApplication(Context application) {
Log.d(TAG, "init");
sApplication = application;
}
}
ModuleTemplateServiceImpl:ModuleTemplateService的实现类,具体的逻辑在此处实现,外部module依赖ModuleTemplateService,而不是ModuleTemplateServiceImpl
@Route(path = ModuleTemplateApiScheme.MODULE_SERVICE_NAME)
public class ModuleTemplateServiceImpl implements ModuleTemplateApiService {
@Override
public String getModuleName() {
return Constants.MODULE_NAME;
}
@SuppressLint("LongLogTag")
@Override
public void init(Context context) {
// 第一次ARouter.navigation()调用的时候,会执行init方法,且只会调用一次,可以在这里触发module内部的初始化逻辑
Log.e("ModuleTemplateServiceImpl", "init=" + context);
new ModuleTemplateApplication(context);
}
}
module_template_run
MainActivity:欢迎页,简单的组件页面跳转
ModuleApplication:当我们独立运行组件时,需要一些网络库,图片库一些最基础的初始化时,就可以在这里实现,简单理解就是AndroidApp里面做的事情,需要搬到这里来。现在里面只有Arouter和网络库的初始化逻辑,如果需要拆分IM或是直播组件,则这里初始化的SDK就需要增加了。
public class ModuleApplication extends ApiApplication {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
EnvConfig.init(isDebug, 1, "1.0", "Android");
AppInit.beforeInit(BuildConfig.DEBUG);
}
@Override
public void onCreate() {
super.onCreate();
// 尽可能早,推荐在Application中初始化
ARouter.init(this);
AppInit.onInit(BuildConfig.DEBUG);
// 初始化ModuleTemplateRun组件
ARouter.getInstance().build(ModuleTemplateApiScheme.MODULE_SERVICE_NAME).navigation();
}
}
module创建之一键生成
在MaltBaby-Android的根目录下,我写了一个shell脚本"moduleCreate.sh",只要在根目录下输入:"./moduleCreate.sh module_a",就会根据module_template的项目结构,创建出module_a,module_a_api,module_a_run三个标准的组件化module。同步下build.gradle文件后,就会在app执行框中看到module_a_run,直接运行即可。
问题
组件整理、抽取
新module创建:如果当前需要创建的module是新的功能,则可以根据module_template直接copy即可。
老module抽取:如果当前需要从app中拆分老的代码成为module,则成本较高,需要将module依赖的所有类,xml,sdk抽取到module中,并把一些app中用到公用代码,提取到library中,然后定义好api层,供外部调用。
组件间的交互和通信
页面跳转:使用Arouter进行schme跳转。页面跳转时,统一使用scheme还有个好处,就是未来可以进行Native到H5的页面降级。
方法调用(有入参+出参):要调用别的Module的某个方法,并希望有返回值时,则使用Arouter Service实现,其实就是定义一个接口,面向抽象编程。
单向通信(只有入参):如果只是单向的向别的module发个消息,或通知,使用EventBus即可
数据传递:sp,sqlite。比如登录组件中,登录成功后将UserInfo保存在sp中,别的module从sp中直接获取即可。
组件的隔离(代码和资源)
代码隔离(推荐runtimeOnly依赖,不过datdbinding的编译会失败,所以先用implement):组件包括module和module_api两个module,理论上module_api是接口抽象层,module是基于module_api层的具体实现类,组件间相互依赖的时候,不该暴露太多细节,只把需要让外界知道的通过api层对外暴露即可。这样做是为了避免组件间产生不必要的耦合,毕竟组件化的收益之一就是降低耦合。组件代码相互隔离的较好的情况下,未来替换或更新组件才能成为可能。例如:播放器组件,只在api层对外暴露,播放,暂停等常用接口,至于内部实现是阿里云,还是七牛,外界不需要知道,就可以更方便的实现组件替换。
资源隔离:不同module的资源可能重命,可以通过resourcePrefix,给每个module添加资源前缀的校验,如果某个jpg,或是xml资源,没有按照这个前缀规则命名,就会报红色警告。
TODO
将app中的业务代码拆分成module,最后app应该只是个壳工程,负责将不同module引入并串联起来
将所有module上传到maven,不同module之间依赖抽象api,不依赖具体实现,每个module支持版本升级,降级。
每个组件module单独编译运行,若依赖其他若干module,可以按需引入,若干个组件module组合后,编译运行。
B/C端APK打包时,只依赖各自业务module