Gradle强制指定模块依赖方式

501 阅读1分钟

当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的形式被依赖,要达到目的,马上可以想到方案的有以下:

  1. 子模块把对log的依赖方式改成源码依赖,重新编译aar。
  2. 子模块把对log的依赖方式改成compileOnly,重新编译aar。
  3. 子模块把对log的依赖方式改成源码依赖,对子模块的所有依赖临时改成源码依赖。
  4. 子模块把对log的依赖方式改成compileOnly,对子模块的所有依赖临时改成源码依赖。
  5. 在子模块的依赖处exclude去掉对log模块的依赖。

方案1和2,弊端在于重新需要重新编译aar,代价太高,而且很可能会影响其它协同开发者。pass。 方案3和4,弊端在于影响扩散效应可能导致修改的模块数量巨大。假设依赖log的模块有n个,而这n个模块的分别又有m个模块依赖他们,只考虑两层依赖的情况下,就会出现n*m处依赖的修改,是个极恐怖的改动量了。pass。 方案5,影响和代码侵入性是在5个方案中最小的,假设依赖log的模块有n个,只需要修改n次即可。

即便是使用方案5还是要改n次,很烦啊,有没有什么一劳永逸的办法呢,比如只通过一个配置文件就能指定某个模块是通过源码依赖还是maven依赖?答案是肯定的。

基本思路是这样:

  1. 在配置文件中指定某些模块的名字、源码位置、maven依赖路径、依赖方式。
  2. settings.gradle中读取该配置,如果模块的依赖方式是源码依赖,则在settings中声明该模块及模块源码位置。
  3. 在主模块的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文件,只得作罢。

至此,功成。