组件化调研与实践

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

你可能感兴趣的:(组件化调研与实践)