Android 组件化架构-2021|8月更文挑战

1,482 阅读7分钟

一、背景

随着项目不断迭代,业务功能不断越多,代码量越来越多,开发人员数量也越来越多。此过程中,你是否有过以下烦恼?

1. 项目模块多且复杂,编译一次要2分钟甚至10分钟?太慢不能忍?
2. 改了一行代码 或只调了一点UI,就要run整个项目,再忍受一次10分钟?
3. 做一个需求,发现还要去改动很多别人模块的代码?
4. 别的模块已实现的类似功能,自己要用只能去复制一份代码再改改?
5. “这个不是我负责的,我不管”,代码责任范围不明确?
6. 做了个需求,但不知不觉导致其他模块出现bug?

如果有这些烦恼,下面的方案可以更好帮您的解决。网上组件化方案实现方式有很多种,但今天讲的这种方案也许会给你带来不一样的认识。

1.1组件化定义

基于可重用为目的,将app拆分成多个模块,每一个模块都是一个组件,开发过程中可以让这些组件相互依赖,也可以单独调试,最终打包时需要合并统一成一个apk。

1.2模块化定义

一种软件设计技术,将功能拆分,分成相互独立的模块。模块可以单独运行,可以包含多个组件。比如:首页模块包含地图、支付、分享组件。相对于组件来说粗粒度更大。 项目大部分都是模块化和组件化一起使用的,通过把业务功能拆成各个模块,进行组件化分层管理。

组件化带来的好处 就显而易见了:

  1. 加快编译速度:每个业务功能都是一个单独的工程,可独立编译运行,拆分后代码量较少,编译自然变快。
  2. 提高协作效率:解耦 使得组件之间 彼此互不打扰,组件内部代码相关性极高。 团队中每个人有自己的责任组件,不会影响其他组件;降低团队成员熟悉项目的成本,只需熟悉责任组件即可;对测试来说,只需重点测试改动的组件,而不是全盘回归测试。
  3. 功能重用:组件 类似我们引用的第三方库,只需维护好每个组件,一建引用集成即可。业务组件可上可下,灵活多变;而基础组件,为新业务随时集成提供了基础,减少重复开发和维护工作量。

二、实战

2.1 架构图

image.png

根据上图搭建出来的项目结构如下图,

2.2 项目结构图

image.png

feature 是一个文件夹,在其里面分别建了3个业务文件夹loginhomecart,每个业务文件夹下管理后缀名为pkgexport的2个模块.
pkg模块负责具体业务逻辑代码.
export模块负责pkg模块暴露的接口层,pkg模块相互之间是不能访问的,export模块相互之间都可以访问login_pkg 只能访问 login_export,因为要实现login_export 里的api。 mock模块可以访问export层,

2.2.1 先在setting.gradle里定义所有的module

image.png

2.2.2 然后。。。。

打住。。。。。如果是这样的实现方式,就没必要讲了。。。

image.png

2.3 具体实现

2.3.1 统一管理第3方依赖

到目前为止管理 Gradle 依赖有 4 种不同方法:

  • 手动管理 :在每个 module 中定义插件依赖库,每次升级依赖库时都需要手动更改(不建议使用)。
  • 使用 ext 的方式管理插件依赖库 :这是 Google 推荐管理依赖的方法 文档,适合小项目。
  • buildSrc:自动补全和单击跳转,依赖更新时 将重新 构建整个项目。
  • Composing builds:自动补全和单击跳转,依赖更新时 不会重新 构建整个项目。

buildSrc的特点

  • 优点:根据Gradle文档描述,当运行 Gradle 时会检查项目中是否存在一个名为 buildSrc 的目录。然后 Gradle 会自动编译并测试这段代码,

  • 缺点:是影响构建速度,如果我改了一个版本号,项目中所有的module都将重新编译,这个代价是非常昂贵并耗时的。

所以我们采用 Composing builds方式管理依赖。具体可参考Gradle文档

image.png image.png 新建一个文件夹,取名BuildSrc,定义Config.groovy类,所有的第3方依赖,定义在defConfig里

2.3.2 统一管理本地模块

image.png 在根目录下创建Config.json,项目中有多少个本地模块全部都要在moduleConfig定义

2.3.3 自定义任意模块启动

上图中,在 appConfig 中可以自定义任意业务模块单独启动了。 image.png
有2个app入口,分别是launcherhome,但是无论哪个app运行,只会把home_pkg打包进apk,如果要把cart_pkg业务模块也打包进apk,可以在pkgConfig里加上 “cart”。如果我想运行home模块,但是home模块包含了logincart

image.png
这时打包后的apk里会包含home_pkg、cart_pkg、login_pkg3个业务模块。

三、构建流程

3.1 从setting.gradle开始

3.1.1 解析config.json

Gradle build的生命周期主要分为三大部分:
初始化阶段 对应:setting.gradle
配置阶段 对应:各个工程对应的build.gradle
执行阶段 执行每个工程的Task

settings.gradle脚本执行的目的是:读取include信息,确定有多少个Project需要构建。

image.png

3.1.2 把moudle 插入至Config.goovy类中

通过上面的操作,我们告诉了Gradle,项目中包含哪些模块。 没有使用组件化时,咱们在app模块引入base模块,通过在build.gradle

dependencies {
    api project(':base')
}

咱们在config.json的moduleConfig节点中统一定义了module,但是如果app模块想引入base模块,还是得通过上面硬编码的方式。为了让重复的事情不做第2遍。

准备在setting.gradle里把定义好的module动态插入到Config.goovy类中。

image.png 上面代码的作用是把config.json中配置的module转换成List<String>

image.png

list中的module 插入到

image.png 红色区域就是上面代码执行后自动插入的,值得注意的是/*Never delete this line*/ 这2行注释不要删除,因为得把代码插入到这2行注释的中间。
最终

dependencies {
    api Config.depConfig.lib_base.dep
}

3.2 build.gradle构建

gradle构建项目时,会先解析项目最外层的build.gradle

3.2.1 项目最外层build.gradle

image.png

3.2.1.1 ConfigUtils.init(gradle) gradle对象是单例,在gradle文件里可以获取

image.png

image.png

image.png

该方法作用是遍历Config.groovy类里的depConfig,获取DepConfig对象,根据useLocal给每个DepConfig设置dep属性。目的是给模块添加依赖时可以直接使用。

 dependencies {  
    api Config.depConfig.lib_base.dep  
}

image.png

重点重点重点,如果模块是login_apphome_appcart_app,构建这些模块时,会先执行buildApp.gradle里的内容,所以:每个app模块内的build.gradle 将会空空如也。 同理,login_exporthome_exportcart_exportxx_pkgmock等模块构建时都会先执行buildLib.gradle,所以这些模块内的 build.gradle 也可以空空如也。

TaskDurationUtils.init(gradle,1000) // 记录build时长超过1000ms的task

image.png

3.2.2 每个模块的build.gradle

xx_app 模块下的build.gradle构建前 会先执行 buildApp.gradle
xx_xx 模块下的build.gradle构建前 会先执行 buildLib.gradle

3.2.3 buildApp.gradle

image.png

  • 每个app都会先执行buildApp.gradle
  • 执行前会先执行 buildCommon.gradle
  • 添加统一插件依赖
  • 剩下的就和普通项目中app模块下的build.gradle一样了,自由发挥。

3.2.4 buildLib.gradle

image.png

重点重点重点,让 pkgmock 模块 依赖所有的export模块,
export 模块 依赖 common 模块

image.png

你看,这是不是就和该架构图对应上了。

3.2.5 buildCommon.gradle

这里就配置每一个模块的通用属性了。

apply {
    plugin "kotlin-android"
    plugin "kotlin-parcelize" 
    plugin 'kotlin-kapt' 
}
android {
    compileSdkVersion Config.compileSdkVersion
    defaultConfig {
        minSdkVersion Config.minSdkVersion
        versionCode Config.versionCode
        versionName Config.versionName
    }
}

四、结尾

pkgexportmockapp模块之间的依赖关系讲完了。这种思想类似 java 后台中的微服务, 每个构件暴露出来的是接口,每个构件可以单独部署。同样,咱们loginhomecart暴露出的接口是export层,每个模块可以单独运行。

4.1 优点

  • 统一管理 Gradle,减少了模块下build.gradle的重复代码
  • 统一管理 依赖 和 插件,全局通过变量引用。
  • 每个业务模块可以单独运行
  • 自由切换源码和远程仓库,可以在config.json里进行配置

4.2 缺点

  • 增加了很多模块,比如:每种业务有pkg、export、app模块

4.3 需要改进的点:

  • 业务pkg模块使用 aar依赖,提高编译速度

  • 多渠道打包支持

  • ...

demo
参考