Szhangbiao's blog

记录一些让自己可以回忆的东西

0%

项目组件化工程配置

回顾整个职业生涯,上一次对项目进行可独立运行的组件化工程改造还是七年前,当时对项目进行第三次重构,想要引入当时比较火的组件化的配置,受限于参考资料比较少,再加上业务上堆积了其他很多的事情,后面对这块也就放弃了,时间来到现在,新项目要求每个人单独负责自己的功能模块且互不影响,这就很符合组件化的概念,于是决定在这个项目中把组件化重拾起来。

背景

我对 SVN 使用要追溯到 14 年且使用的程度也不深,15 年换了工作后就没再接触了,印象中遇到代码冲突是比较麻烦的。新加入的公司代码管理工具一直是 SVN,为了避免组员之间的代码冲突,会把工程进行拆分,首先按照人员在工程目录下创建对应的子工程目录,每个人在自己的所属工程目录下按照所分配的业务功能来进行模块化拆分,在模块功能开发完后就把他推到私有 maven 服务器里供自己或其他成员的模块引用,以此在物理层面实现成员之间代码的隔离。

这样做的好处就是:

  • 成员之间的代码互不干扰,可以在自己的工程下自由发挥
  • 业务模块划分比较清晰,后续如果出问题也方便定责
  • 物理层面进行代码隔离,彻底避免代码冲突出现的可能

当然也有缺点:

  • 每个子工程就是一个独立的 Android 工程,需要额外配置app模块
  • 成员的开发能力不一致,也造就了开发规范和技术栈不一致,代码管理相对混乱
  • 模块间沟通成本较高且出现问题Debug时间也会变长
  • 成员之间并不会互相关注对方代码,只关注功能实现,不能互相借鉴学习

因此我在接受重构项目的任务后,为了兼顾之前的项目特点,决定使用组件化对工程进行改造

思考过程

组件化的基础是模块化,站在整体的角度上把工程拆分为多个模块,按照模块的功能和职责划分的话,其中一些模块里封装了特定的业务逻辑和 UI,这些模块就可以称为组件,组件模块除了满足高内聚、低耦合和可独立替换或更新的特点外,在某些情况下,可以独立运行和部署。

在 Android 工程层面,一个模块可以单独运行和调试,那它的模块类型必须是application,如果它想被其他模块引用,那它的模块类型必须是library,于是就有了以下样板代码:

1
2
3
4
5
if(isComponent){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}

可独立运行的组件还需要一些额外的配套,像作为application时所必须的AndroidManifest.xmlApplication类和入口Activity

技术要点

以下每一个技术点单拎出来都可以长篇大论一番,这里我们只做简单的介绍

三方库管理工具 Catalogs

Gradle 7.0 引入的一种新特性,以可扩容的方式添加和维护依赖项和插件。

  • 提高 build 文件的可读性和一致性,减少重复和冗余的代码
  • 支持代码补全和导航,提高开发效率和准确性
  • 方便地在一个地方管理依赖项和插件的版本,避免版本冲突和不匹配的问题
  • 支持多项目构建和复用,提高构建性能和稳定性

Kotlin Script

Kotlin Script简称KTSGradle 4.0支持在build.gradle配置中使用 Kotlin,用于替代Groovy

  • 采用 Kotlin 编写的代码可读性更高
  • 提供了更好的编译时检查和 IDE 支持
  • 能更好地在 Android Studio 的代码编辑器中集成

自定义Gradle插件

相对于在gradle.properties里定义一个变量来控制模块的类型,自定义gradle插件的方式更加灵活,封装的比较好的话也可以发布到远程分享出去

编写自定义插件目的主要是参与编译构建 Android 工程,去完成一些特定的功能

主要分为两种:

1.脚本插件

创建一个.gradle后缀的文件,通过apply from引入,然后在build.gradle中调用插件

2.对象插件

主要是实现org.gradle.api下的Plugin接口,有三种编写形式:

  • build.gradle文件中直接编写
  • buildSrc默认插件目录下编写
  • 在自定义模块中编写

这里为了配合catalogs的使用,我们使用的是对象插件且是在自定义模块下编写自定义插件

模块AAR

工程模块化会把工程代码分散到一个个独立的模块中,增加内聚降低耦合的同时也方便后续管理维护,但是带来的副作用是一次工程的全量编译会花费非常多的时间,模块的数量越多,花费时间越长。

目前普遍的解决方案是把模块预编译成AAR文件,然后在主模块中引用,这里主要用到maven-push这个三方插件,它支持把生成的AAR文件推到本地或者远程仓库,然后在主工程里像使用第三方库一样去使用我们的模块。

maven-push的配置使用的是以脚本插件的方式引入,主要的代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
//script/maven-publish.gradle
apply plugin: 'maven-publish'

tasks.withType(PublishToMavenRepository).tap {
configureEach { task ->
// 第一个参数是match渠道,第二个是match类型(MavenRepository、MavenLocal)
def match = task.name =~ '^publish(.*)(Release|Debug)PublicationTo(.*)$'
dependsOn("assemble${match[0][1]}")
}
}
// 这里我们使用一个变量控制maven推送到本地还是远程
def useMavenLocal = true

afterEvaluate {
publishing {
def name = project.name
def mavenPublish = Config.MODULES_MAVEN_CONFIG.get(name)
if (mavenPublish != null) {
def versionName = mavenPublish.version
publications {
release(MavenPublication) {
groupId = mavenPublish.groupId
artifactId = mavenPublish.artifactId
version = mavenPublish.version
def buildType = versionName.endsWith('SNAPSHOT') ? 'debug' : 'release'
artifact "build/outputs/aar/${name}-${buildType}.aar"
pom.withXml {
def dependenciesNode = asNode().appendNode('dependencies')
def scopes = [configurations.implementation]
if (configurations.hasProperty("api")) {
scopes.add(configurations.api)
}
if (configurations.hasProperty("implementation")) {
scopes.add(configurations.implementation)
}
if (configurations.hasProperty("debugImplementation")) {
scopes.add(configurations.debugImplementation)
}
if (configurations.hasProperty("releaseImplementation")) {
scopes.add(configurations.releaseImplementation)
}
scopes.each { scope ->
scope.allDependencies.each {
if (it instanceof ModuleDependency) {
boolean isTransitive = ((ModuleDependency) it).transitive
if (!isTransitive) {
return
}
}
if (it.group == "${project.rootProject.name}.libs" || it.version == 'unspecified') {
return
}
if (it.group && it.name && it.version) {
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', it.group)
dependencyNode.appendNode('artifactId', it.name)
dependencyNode.appendNode('version', it.version)
dependencyNode.appendNode('scope', scope.name)
}
}
}
}
}
}
repositories { RepositoryHandler handler ->
if (useMavenLocal) {
maven {
url = uri("${project.rootDir}/.mavenLocal/repository")
}
} else {
maven {
allowInsecureProtocol = true
url = versionName.endsWith('SNAPSHOT') ? Config.MAVEN.snapshotUrl : Config.MAVEN.releaseUrl
credentials {
username = Config.MAVEN.credentials.username
password = Config.MAVEN.credentials.password
}
}
}
}
}
}
}

其中的Config类定义在buildSrc下,用于定义阿里云效Maven的配置以及需要发布到阿里云效下Maven的模块配置信息。

然后在项目级的build.gradle.kts中引入一下

1
2
3
4
5
6
// build.gradle.kts
...
subprojects {
apply("${project.rootDir}/script/maven-publish.gradle")
}
...

这里的脚本插件之所以使用groovy语言,是因为KTS不支持用来写脚本插件,写完之后无法像.gradle文件那样在build.gradle.kts中引入,所以这里使用groovy语言来实现

代码实施

结合上面的技术要点和项目的情况,我针对模块的类型定义了三个类型的gradle插件

  • App:在工程里主要是指:app模块
  • Component:主要是指:feature:*模块,可以通过配置来决定是 Application 还是 Library
  • Lib:在工程里主要是指:core:*模块

AppLib插件类分别封装了appcore模块下build.gradle.kts所需要的配置,像常见的buildTypecompileOptionsproductFlavorssigningConfigssourceSetsDependencies等等

Component插件类封装了feature模块下build.gradle.kts所需要的配置以及根据配置来决定是Application还是Library的配置,这个额外的配置有两个开关,一个总开关和各个组件模块下的子开关,总开关在插件工程的相应的代码里,子开关则放在组件模块下的local.properties里且把这个文件加入到.gitignore里,这样组员打开总开关后,可以准对不同组件模块在本地做不同的配置且不互相影响

以下是插件工程的相关配置:

1
2
3
4
5
build-logic
─── convention
├── src/main/kotlin # 自定义插件以及相关配置的代码
└── build.gradle.kts
settings.gradle.kts

settings.gradle.kts里主要引入了catalogs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// build-logic/settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "build-logic"
include(":convention")

convention下的build.gradle.kts里主要定义了插件的注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// build-logic/convention/build.gradle.kts
plugins {
`kotlin-dsl`
}
repositories {
google()
mavenCentral()
// google() 国内阿里云替代
maven { url = uri("https://maven.aliyun.com/repository/google") }
}

dependencies {
implementation(libs.android.gradle.plugin)
implementation(libs.android.tools.common)
//gradle sdk
implementation(gradleApi())
//groovy sdk
implementation(localGroovy())
}

gradlePlugin {
plugins {
plugins {
register("androidComponent") {
id = "com.build.component"
implementationClass = "plugins.ComponentBuildPlugin"
version = "1.0.0"
}
}
}
...
}

下面主要给出Component插件的伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// build-logic/convention/src/main/kotlin/plugins/ComponentBuildPlugin.kt
class ComponentBuildPlugin : Plugin<Project> {
override fun apply(project: Project) {
with(project) {
// 根据条件判断应用该插件的模块是Application还是Library
if(xxx) {
pluginManager.apply("com.android.application")
} else {
pluginManager.apply("com.android.library")
}
// sourceSets 配置
sourceSets {
getByName("main") {
if(xxx) {
manifest.srcFile("src/component/AndroidManifest.xml")
java.srcDirs("src/main/java", "src/component/java")
assets.srcDirs("src/main/assets", "src/component/assets")
jniLibs.srcDirs("src/main/jniLibs", "src/component/jniLibs")
} else {
manifest.srcFile("src/main/AndroidManifest.xml")
java.srcDirs("src/main/java")
assets.srcDirs("src/main/assets")
jniLibs.srcDirs("src/main/jniLibs")
}
}
}
}
}
}

从代码可以看出,这里主要把组件模块独立运行所需要的相关资源,放到模块下main同级别的component下了

延伸

还是本着配置的原则,支持模块间的协作与高效效率开发,这里通过代码来实现

模块作为Application级别的模块时,可以配置当前模块对其他模块的依赖方式:

1
2
3
4
5
enum class CompileMode {
LIBRARY,
HYBRID,
PROJECT
}

针对不同的模块做类的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
data class App(
val applicationId: String,
val compileMode: CompileMode = CompileMode.LIBRARY,
// 当compileMode为Hybrid时, feature 以哪种形式在App中引入 true library() false project()
val featureFlags: Map<String, Boolean> = mapOf("main" to true,...),
// 当compileMode为Hybrid时, library 以那种形式在App中引入 true library() false project()
val libraryFlags: List<Library> = listOf(
Library("model", true),
...
)
)

data class Features(
val name: String,
val applicationId: String,
val compileMode: CompileMode = CompileMode.PROJECT,
// 当compileMode为Hybrid时, library 以那种形式在App中引入 true library() false project()
val libraryFlags: List<Library> = listOf(
Library("model", true),
...
)
)

data class Library(val name: String, val libraryFlag: Boolean)

在结构化后再处理对应的依赖关系,最后给出三种模式的关系图:
image

总结

结合一些我看过的有关组件化开发的文章,我个人还是比较倾向于使用这种内置在工程内部的组件化方案

主要有以下优点:

  • 每个模块通用的gradle配置可以放到自定义插件中,提高可维护性
  • 针对项目的特点或者某个特别的模块可以做一些额外的处理,随时改动立马就见效果
  • 比较灵活,可以通过配置来实现不同的方式,既可以全量编译,又可以部分模块参与编译运行

参考