Android组件化的绝配Composite Build
1. 什么是Composite Build
Composite Build可以简单的认为是一个Build包含其他多个Build。这个是官方的说法,如果从Android开发的角度来看,可以理解为,一个Project可以包含其他Project。
正常情况下,使用Android Studio开发的时候,可以通过New Project的方式创建一个新的Project。一个Project内部会包含多个Module,不同的Project之间是没有联系的。不过如果使用了Composite Build,那么会在不同的Project之间建立联系。
这个联系是通过依赖替换来生成的,可以指定某一个依赖替换为其他Project中的Module。这样原本是implementation 'com.example.xx:1.0.0'
的依赖方式,会替换为implementation project('xx')
的依赖方式。这样就可以进行调试、查看代码、Refactor等操作。
2. 为什么使用Composite Build
在平时的开发中,在项目刚开始的时候,一般只会有一个主Module,有新需求、新功能都在这个主Module 中添加就好了。
但是随着越来越多的功能被添加进来,这个Module会变得越来越臃肿。为了更好地对项目进行管理,这时候会创建多个Module来分担主Module的压力。这时候主Module对其他Module之间的依赖是通过implementation project('')
的方式来引用的。这种方式后面都称为project方式。
但是随着越来越多的Module被加入,编译速度越来越慢,而且可能某些Module需要被别的项目使用。为了提高编译速度、提高Library的复用性,这时会搭建一个Maven仓库,将Module发布到仓库上,然后通过implementation 'com.example.xx:1.0.0'
的方式来引用Module。这种方式后面都成为maven方式。这样就会避免project方式每次编译过程中每个Module都需要重新编译的缺点,直接从Maven仓库获取已经发布的aar/jar,加快编译速度。
使用Maven方式确实可以提高编译速度,也提高了复用性。但是如果某一个已经发布的Library代码需要修改,这时候就麻烦了。
- 将对目标Library的依赖方式从maven方式改为project方式
- 修改代码,并进行调试验证
- 修改版本号,发布新版本到仓库
- 修改对目标Library有依赖关系的Library的版本号,发布它们的新版本到仓库
- 对4中修改了版本号的Library重复4的步骤,直到所有对目标Library有直接或者间接依赖关系的Library都发布了新的版本到仓库
或者使用snapshot的方式替换上述的1,2步骤。
可以看到,这是一个非常复杂的步骤,而且如果一旦疏忽,就可能导致工程中存在两个版本号的目标Library。
那么如何解决上面的问题呢?答案就是使用Composite Build。
3. 如何实现Composite Build
在使用Composite Build之前,我们的工程目录是如下所示的,只有MyApp这个工程下面有settings.gradle文件,这个文件中声明了app、lib1、lib2这三个Module。app依赖了lib1和lib2。
MyApp/
├── build.gradle
├── settings.gradle
├── app
│ ├── build.gradle
│ └── src
├── lib1
│ ├── build.gradle
│ └── src
└── lib2
├── build.gradle
└── src
如果直接在这个结构上使用Composite构建是不可以的,因为Composite构建要求所有参与Composite构建的目录下面必须有settings.gradle文件。如果app这个Module通过Composite的方式依赖lib1和lib2,那么就需要做一些处理:
- 在MyApp同级的目录下,创建两个新的文件夹,lib1Project、lib2Project。在新文件夹中均放入build.gradle和settings.gradle两个文件,相当于创建了两个Project工程,然后将lib1和lib2分别放入这两个文件夹中。如果想省事,可以直接使用Android Studio创建两个新的Project。
- 修改lib1Project中的settings.gradle,在其中声明lib1这个Moule,lib2Project同样做法。
- 修改lib1Project和lib2Project的build.gradle文件,配置repositories和dependencies。这里需要注意,需要保证这两个文件中的dependencies和MyApp中的dependencies保持一致,顺序也必须一致。否则编译会出错,可以参考 https://developer.android.com/studio/known-issues
Composite Builds条目。 - 移除MyApp目录下settings.gradle中对lib1和lib2的声明,并在settings.gradle中添加如下代码
includeBuild('../lib1Project')
includeBuild('../lib2Project')
- 在MyApp工程中,sync gradle,sync成功后,在Android Studio的project视图下可以看到MyApp同级的目录下出现了lib1Project和lib2Project,这时候就说明composite构建成功了。虽然此时app对lib1和lib2的依赖方式在其build.gradle文件中仍然是maven方式的,但是实际上此时已经变成了project的方式。
- 如果在5的步骤中,没有看到出现lib1Project和lib2Project,说明此时构建失败了,其中的一个可能的原因在于,发布lib1和lib2到maven的时候,修改了其默认的GROUP和ARTIFACTID。这个时候就需要在includeBuild的时候,手动指定要替换的library如下:
includeBuild('../lib1Project'){
dependencySubstitution {
substitute module('com.example.xx:lib1') with project (':lib1')
}
}
includeBuild('../lib2Project'){
dependencySubstitution {
substitute module('com.example.xx:lib1') with project (':lib2')
}
}
此时的工程目录如下
rootDir/
├── MyApp/
│ ├── build.gradle
│ ├── settings.gradle
│ ├── app
│ ├── build.gradle
│ └── src
├── lib1Project/
│ ├── build.gradle
│ ├── settings.gradle
│ ├── lib1
│ ├── build.gradle
│ └── src
├── lib2Project/
├── build.gradle
├── settings.gradle
├── lib2
├── build.gradle
└── src
但是在不需要调试和修改代码的时候。并不想使用Composite构建怎么办呢?
如果通过在gradle文件中增减字段作为变量来判断是否使用Composite构建的方式固然可以,但是可能存在不小心将修改后的字段提交,导致CI上打包使用了Composite构建。
这时候可以考虑在lib1Project/lib2Project目录下创建一个名为.composite-enable
的隐藏文件,通过判断这个文件是否存在来决定是否使用Composite构建方式。为了避免不小心将这个文件上传到服务器,可以将该文件添加到gitignore中,这样该文件就只会在本地存在,而不会影响到CI打包。
为了更加方便地创建和删除文件,可以在gradle文件中添加Task来辅助处理。
创建文件
task enable Lib1Project CompositeBuild(){
group = 'Tools'
description = 'Enable Lib1Project composite build'
doLast {
if (file("../Lib1Project").exists()) {
println("create .composite-enable file")
new File("../Lib1Project/.composite-enable").createNewFile()
}
}
}
删除文件
task disable Lib1Project CompositeBuild(){
group = 'Tools'
description = 'Disable Lib1Project composite build'
doLast {
if (file("../Lib1Project").exists()) {
File file = file("../Lib1Project/.composite-enable")
if (file.exists()) {
println("delete .composite-enable file")
file.delete()
}
}
}
}
4. Composite Build 和组件化
现在组件化非常火,那么组件化配合Composite构建将能摩擦出更大的火花,每一个组件可以放到一个单独的Project中,然后通过Composite构建的方式调试修改代码。可以避免在一个Project中存在过多Module的情况。
rootDir/
├── MainApp/
│ ├── build.gradle
│ ├── settings.gradle
│ ├── app
│ ├── build.gradle
│ └── src
├── ComponentAProject/
│ ├── build.gradle
│ ├── settings.gradle
│ ├── app
│ │ ├── build.gradle
│ │ └── src
│ ├── ComponentALibrary
│ │ ├── build.gradle
│ │ └── src
│ ├── ComponentAService
│ ├── build.gradle
│ └── src
├── ComponentBProject/
│ ├── build.gradle
│ ├── settings.gradle
│ ├── app
│ │ ├── build.gradle
│ │ └── src
│ ├── ComponentBLibrary
│ │ ├── build.gradle
│ │ └── src
│ ├── ComponentBService
│ ├── build.gradle
│ └── src
├── Common/
├── build.gradle
├── settings.gradle
├── CommonLibrary
├── build.gradle
└── src
假如目前组件化项目中有两个组件:ComponentA和ComponentB,一个基础库Common,一个MainApp。
- MainApp通过Maven的方式依赖ComponentA、ComponentB和Common,打包生成主App
- ComponentA内部有app Module作为组件的壳,ComponentALibrary包含A的主要业务,ComponentAService包含对外的接口,其他组件如果需要与ComponentA进行通信的话,可以依赖ComponentAService。
- ComponentB同A
- ComponentA通过Maven方式依赖了ComponentBService和ComponentB进行通信,依赖了Common
- ComponentB通过Maven方式依赖了ComponentAService和ComponentA进行通信,依赖了Common
- 在需要对代码进行调试的时候,可以通过配置Composite Build的方式,将Maven的依赖替换为Project依赖的方式。
- Component对外的接口不需要下沉到一个非常低的层级,让所有的组件都来依赖,而是按需依赖。一旦发生了修改,不会所有的组件都会受影响,而只会影响对其有依赖的组件。
- 每个组件在自己的Project中有独立的APP Module,也方便其独立安装运行。
- 使用Composite Build方式,避免了在一个Project工程中,存在过多的Module,导致编译和运行非常缓慢;也避免了即使将这些Module发布到Maven上,导致后面调试维护非常麻烦的缺点。
这样每一个组件都在自己的Project中,方便进行代码管理和权限管理。
参考文章
Composite builds
Gradle之多项目与混合构建 – Android开发中文站
如何简单快速搭建 Android 大仓 - Yrom’s
Saying Goodbye to SNAPSHOTs with Gradle’s Composite Builds