一、背景
随着项目不断迭代,业务功能不断越多,代码量越来越多,开发人员数量也越来越多。此过程中,你是否有过以下烦恼?
1. 项目模块多且复杂,编译一次要2分钟甚至10分钟?太慢不能忍?
2. 改了一行代码 或只调了一点UI,就要run整个项目,再忍受一次10分钟?
3. 做一个需求,发现还要去改动很多别人模块的代码?
4. 别的模块已实现的类似功能,自己要用只能去复制一份代码再改改?
5. “这个不是我负责的,我不管”,代码责任范围不明确?
6. 做了个需求,但不知不觉导致其他模块出现bug?
如果有这些烦恼,下面的方案可以更好帮您的解决。网上组件化方案实现方式有很多种,但今天讲的这种方案也许会给你带来不一样的认识。
1.1组件化定义
基于可重用为目的,将app拆分成多个模块,每一个模块都是一个组件,开发过程中可以让这些组件相互依赖,也可以单独调试,最终打包时需要合并统一成一个apk。
1.2模块化定义
一种软件设计技术,将功能拆分,分成相互独立的模块。模块可以单独运行,可以包含多个组件。比如:首页模块包含地图、支付、分享组件。相对于组件来说粗粒度更大。 项目大部分都是模块化和组件化一起使用的,通过把业务功能拆成各个模块,进行组件化分层管理。
组件化带来的好处 就显而易见了:
- 加快编译速度:每个业务功能都是一个单独的工程,可独立编译运行,拆分后代码量较少,编译自然变快。
- 提高协作效率:解耦 使得组件之间 彼此互不打扰,组件内部代码相关性极高。 团队中每个人有自己的责任组件,不会影响其他组件;降低团队成员熟悉项目的成本,只需熟悉责任组件即可;对测试来说,只需重点测试改动的组件,而不是全盘回归测试。
- 功能重用:组件 类似我们引用的第三方库,只需维护好每个组件,一建引用集成即可。业务组件可上可下,灵活多变;而基础组件,为新业务随时集成提供了基础,减少重复开发和维护工作量。
二、实战
2.1 架构图
根据上图搭建出来的项目结构如下图,
2.2 项目结构图
feature
是一个文件夹,在其里面分别建了3个业务文件夹login
、home
、cart
,每个业务文件夹下管理后缀名为pkg
、export
的2个模块.
pkg
模块负责具体
业务逻辑代码.
export
模块负责pkg
模块暴露的接口层,pkg
模块相互之间是不能访问
的,export
模块相互之间都可以访问
。login_pkg
只能访问 login_export
,因为要实现login_export
里的api。
mock
模块可以访问export
层,
2.2.1 先在setting.gradle里定义所有的module
2.2.2 然后。。。。
打住。。。。。如果是这样的实现方式,就没必要讲了。。。
2.3 具体实现
2.3.1 统一管理第3方依赖
到目前为止管理 Gradle 依赖有 4 种不同方法:
- 手动管理 :在每个 module 中定义插件依赖库,每次升级依赖库时都需要手动更改(不建议使用)。
- 使用 ext 的方式管理插件依赖库 :这是 Google 推荐管理依赖的方法 文档,适合小项目。
- buildSrc:自动补全和单击跳转,依赖更新时 将重新 构建整个项目。
- Composing builds:自动补全和单击跳转,依赖更新时 不会重新 构建整个项目。
buildSrc的特点
-
优点:根据Gradle文档描述,当运行 Gradle 时会检查项目中是否存在一个名为 buildSrc 的目录。然后 Gradle 会自动编译并测试这段代码,
-
缺点:是影响构建速度,如果我改了一个版本号,项目中所有的module都将重新编译,这个代价是非常昂贵并耗时的。
所以我们采用 Composing builds
方式管理依赖。具体可参考Gradle文档
新建一个文件夹,取名BuildSrc,定义Config.groovy类,所有的第3方依赖,定义在defConfig里
2.3.2 统一管理本地模块
在根目录下创建Config.json
,项目中有多少个本地模块全部都要在moduleConfig
定义
2.3.3 自定义任意模块启动
上图中,在 appConfig 中可以自定义任意业务模块单独启动了。
有2个app入口,分别是launcher
、home
,但是无论哪个app运行,只会把home_pkg
打包进apk,如果要把cart_pkg
业务模块也打包进apk,可以在pkgConfig里加上 “cart”
。如果我想运行home
模块,但是home
模块包含了login
和cart
这时打包后的apk
里会包含home_pkg、cart_pkg、login_pkg
3个业务模块。
三、构建流程
3.1 从setting.gradle开始
3.1.1 解析config.json
Gradle build的生命周期主要分为三大部分:
初始化阶段
对应:setting.gradle
配置阶段
对应:各个工程对应的build.gradle
执行阶段
执行每个工程的Task
settings.gradle脚本执行的目的是:读取include信息,确定有多少个Project需要构建。
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类中。
上面代码的作用是把config.json中配置的module转换成List<String>
把list
中的module
插入到
红色区域就是上面代码执行后自动插入的,值得注意的是/*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
3.2.1.1 ConfigUtils.init(gradle)
gradle对象是单例,在gradle文件里可以获取
该方法作用是遍历
Config.groovy
类里的depConfig
,获取DepConfig
对象,根据useLocal给每个DepConfig设置dep
属性。目的是给模块添加依赖时可以直接使用。
dependencies {
api Config.depConfig.lib_base.dep
}
重点
、重点
、重点
,如果模块是login_app
、home_app
、cart_app
,构建这些模块时,会先执行buildApp.gradle
里的内容,所以:每个app
模块内的build.gradle
将会空空如也
。 同理,login_export
、home_export
、cart_export
、xx_pkg
、mock
等模块构建时都会先执行buildLib.gradle
,所以这些模块内的build.gradle
也可以空空如也。
TaskDurationUtils.init(gradle,1000) // 记录build时长超过1000ms的task
3.2.2 每个模块的build.gradle
xx_app 模块下的build.gradle构建前 会先执行 buildApp.gradle
xx_xx 模块下的build.gradle构建前 会先执行 buildLib.gradle
3.2.3 buildApp.gradle
- 每个app都会先执行
buildApp.gradle
- 执行前会先执行
buildCommon.gradle
- 添加统一插件依赖
- 剩下的就和普通项目中
app模块
下的build.gradle
一样了,自由发挥。
3.2.4 buildLib.gradle
重点
、重点
、重点
,让pkg
和mock
模块 依赖所有的export
模块,
让export
模块 依赖common
模块
你看,这是不是就和该架构图对应上了。
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
}
}
四、结尾
pkg
、export
、mock
,app
模块之间的依赖关系讲完了。这种思想类似 java 后台中的微服务, 每个构件暴露出来的是接口,每个构件可以单独部署。同样,咱们login
、home
、cart
暴露出的接口是export
层,每个模块可以单独运行。
4.1 优点
- 统一管理 Gradle,减少了模块下build.gradle的重复代码
- 统一管理 依赖 和 插件,全局通过变量引用。
- 每个业务模块可以单独运行
- 自由切换源码和远程仓库,可以在config.json里进行配置
4.2 缺点
- 增加了很多模块,比如:每种业务有pkg、export、app模块
4.3 需要改进的点:
-
业务pkg模块使用 aar依赖,提高编译速度
-
多渠道打包支持
-
...