当App代码量较大,子模块层级划分和依赖比较复杂。经常有这样的场景:
- 需求变更或者bug修复,需要修改一个基础模块的代码。
- 这个基础模块被很多其它子模块和主模块依赖着。
- 为了提升打包速度,基础模块都是以aar的方式被依赖的,依赖他的子模块本身也是一个aar被主模块依赖。
为了使修改的代码生效,首先肯定时需要在settings.gradle文件中声明该基础模块,像这样:
include ':log'
project(':log').projectDir = new File("$rootDir/common_lib/log")
这个时候,运行或者打包会报错,大致意思就是log模块中的某个类重复定义了。也很好理解,project(':log')和"com.test.common_lib:log:0.0.1"其实是一个东西,但是Android Studio目前并不知道这回事,打包时会把这两个库里的class都打包到dex中,在校验环节会发现类定义重复从而导致打包失败。
所以我们其实漏掉了一步,要把其它子模块里对log的依赖方式也改成源码依赖,但是,子模块自己也是以aar的形式被依赖,要达到目的,马上可以想到方案的有以下:
- 子模块把对log的依赖方式改成源码依赖,重新编译aar。
- 子模块把对log的依赖方式改成
compileOnly,重新编译aar。 - 子模块把对log的依赖方式改成源码依赖,对子模块的所有依赖临时改成源码依赖。
- 子模块把对log的依赖方式改成
compileOnly,对子模块的所有依赖临时改成源码依赖。 - 在子模块的依赖处exclude去掉对log模块的依赖。
方案1和2,弊端在于重新需要重新编译aar,代价太高,而且很可能会影响其它协同开发者。pass。 方案3和4,弊端在于影响扩散效应可能导致修改的模块数量巨大。假设依赖log的模块有n个,而这n个模块的分别又有m个模块依赖他们,只考虑两层依赖的情况下,就会出现n*m处依赖的修改,是个极恐怖的改动量了。pass。 方案5,影响和代码侵入性是在5个方案中最小的,假设依赖log的模块有n个,只需要修改n次即可。
即便是使用方案5还是要改n次,很烦啊,有没有什么一劳永逸的办法呢,比如只通过一个配置文件就能指定某个模块是通过源码依赖还是maven依赖?答案是肯定的。
基本思路是这样:
- 在配置文件中指定某些模块的名字、源码位置、maven依赖路径、依赖方式。
- 在
settings.gradle中读取该配置,如果模块的依赖方式是源码依赖,则在settings中声明该模块及模块源码位置。 - 在主模块的
build.gradle中读取该配置,借助configurations.all {}遍历整个工程的依赖项,如果依赖项存在与配置文件中,且依赖方式和配置不符,则强制将其依赖方式改成配置文件中指定的方式。
话不多说,直接看代码实现: 首先是配置文件,这里假设配置文件名为force_module.config.json
[
{
"moduleName": "log", // 模块名,不带前面的冒号
"localPath": "./common_lib/log", // 模块源码module目录
"remotePath": "com.test.common_lib:log:0.0.1", // 模块maven依赖路径
"dependencyType": 1 // 强制依赖方式,1:源码;0:maven
}
]
然后是settings.gradle
...
List<HashMap<String, String>> readForceModuleConfig() {
def list = new ArrayList()
if (file('force_module.config.json').exists()) {
def txt = file('force_module.config.json').readLines()
if (txt == null) {
return
}
txt = txt.join('')
def items = txt.findAll(java.util.regex.Pattern.compile('[{]([\\s\\S]*?)[}]'))
if (items != null && !items.isEmpty()) {
items.forEach { item ->
item = item.replace('{', '').replace('}', '').trim()
def kvMap = new HashMap<String, String>()
def kv = item.split(',')
for (i in 0..<kv.length) {
def ii = kv[i].indexOf(':')
if (ii > 0) {
kvMap.put(kv[i].substring(0, ii).replace('"', '').trim(), kv[i].substring(ii + 1).replace('"', '').trim())
}
}
list.add(kvMap)
}
}
}
println(list)
return list
}
// 强制子模块module依赖方式配置
def modules = readForceModuleConfig()
if (modules != null) {
modules.forEach{module ->
if (module != null && module.get("moduleName") != null) {
def moduleName = module.get("moduleName")
if (module.get("dependencyType").toInteger() == 1) {
// 源码依赖
def localPath = module.get("localPath")
if (localPath == null || localPath.isEmpty()) {
println 'force to use source dependency, but field localPath not set'
} else {
include ":$moduleName"
project(":$moduleName").projectDir = file(localPath)
}
}
}
}
}
最后是主模块的build.gradle
android {
...
def modules = readForceModuleConfig()
configurations.all {
if (modules != null && !modules.isEmpty()) {
resolutionStrategy.dependencySubstitution.all { DependencySubstitution dependency ->
def depName
if (dependency.requested instanceof ModuleComponentSelector) {
depName = dependency.requested.module
} else if (dependency.requested instanceof ProjectComponentSelector) {
depName = dependency.requested.projectName
} else {
println "!!!!!!!!!!!!!!!exist unknown component type!!!!!!!!!" + dependency.requested.getClass()
}
def target = modules.find {
return it.get("moduleName") == depName
}
if (target != null) {
if (target.get("dependencyType").toInteger() == 1) {
// 强制源码依赖
if (dependency.requested instanceof ModuleComponentSelector) {
def targetProject = findProject(":${target.get("moduleName")}")
if (targetProject != null) {
println "use local project for $target"
dependency.useTarget targetProject
}
}
} else {
// 强制maven依赖
if (dependency.requested instanceof ProjectComponentSelector) {
def remotePath = target.get("remotePath")
if (remotePath != null && !remotePath.isEmpty()) {
println "use remote project for $target"
dependency.useTarget remotePath
}
}
}
}
}
}
}
}
List<HashMap<String, String>> readForceModuleConfig() {
def list = new ArrayList()
if (file('../force_module.config.json').exists()) {
def txt = file('../force_module.config.json').readLines()
if (txt == null) {
return
}
txt = txt.join('')
def items = txt.findAll(java.util.regex.Pattern.compile('[{]([\\s\\S]*?)[}]'))
if (items != null && !items.isEmpty()) {
items.forEach { item ->
item = item.replace('{', '').replace('}', '').trim()
def kvMap = new HashMap<String, String>()
def kv = item.split(',')
for (i in 0..<kv.length) {
def ii = kv[i].indexOf(':')
if (ii > 0) {
kvMap.put(kv[i].substring(0, ii).replace('"', '').trim(), kv[i].substring(ii + 1).replace('"', '').trim())
}
}
list.add(kvMap)
}
}
}
println "force module: "+list
return list
}
可以看到,两个gradle文件中定义了一个几乎一样的读配置文件的方法,自然而然地会想抽出去放到一个单独的gradle文件中,但是实践证明,settings.gradle并不支持引入gradle文件,只得作罢。
至此,功成。