组件化调研与实践

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容