最近,因为开发的时分经改动依靠的库,所以,我想对 Gradle 脚本做一个调整,用来动态地将依靠替换为源码。这儿以 android-mvvm-and-architecture 这个工程为例。该工程以依靠的方法引证了我的另一个工程 AndroidUtils。在之前,当我需求对 AndroidUtils 这个工程源码进行调整时,一般来说有两种解决方法。

1、一般的修正方法

一种方法是,直接修正 AndroidUtils 这个项意图源码,然后将其发布到 MavenCentral. 等它在 MavenCentral 中生效之后,再将项目中的依靠替换为最新的依靠。这种方法可行,可是修正的周期太长。

另外一种方法是,修正 Gradle 脚本,手动地将依靠替换为源码依靠。此时,需求做几处修正,

修正 1,在 settings.gradle 里面将源码作为子工程增加到项目中,

include ':utils-core', ':utils-ktx'
project(':utils-core').projectDir = new File('../AndroidUtils/utils')
project(':utils-ktx').projectDir = new File('../AndroidUtils/utils-ktx')

修正 2,将依靠替换为工程引证,

// implementation "com.github.Shouheng88:utils-core:$androidUtilsVersion"
// implementation "com.github.Shouheng88:utils-ktx:$androidUtilsVersion"
// 上面的依靠替换为下面的工程引证
implementation project(":utils-core")
implementation project(":utils-ktx")

这种方法亦可行,只不过过于繁琐,需求手动修正 Gradle 的构建脚本。

2、经过 Gradle 脚本动态修正依靠

其实 Gradle 是支撑动态修正项目中的依靠的。动态修正依靠在上述场景,特别是组件化的场景中非常有用。这儿我参考了公司组件化的切换源码的完结方法,用了 90 行左右的代码就完结了上述需求。

2.1 配置文件和作业流程笼统

这种完结方法里比较重要的一环是对切换源码作业机制的笼统。这儿我重新定义了一个 json 配置文件,

[
  {
    "name": "AndroidUtils",
    "url": "git@github.com:Shouheng88/AndroidUtils.git",
    "branch": "feature-2.8.0",
    "group": "com.github.Shouheng88",
    "open": true,
    "children": [
      {
        "name": "utils-core",
        "path": "AndroidUtils/utils"
      },
      {
        "name": "utils-ktx",
        "path": "AndroidUtils/utils-ktx"
      }
    ]
  }
]

它内部的参数的意义分别是,

  • name:工程的称号,对应于 Github 的项目名,用于寻觅克隆到本地的代码源码
  • url:长途库房的地址
  • branch:要启用的长途库房的分支,这儿我强制主动切换分支时的本地分支和长途分支同名
  • group:依靠的 group id
  • open:表明是否启用源码依靠
  • children.name:表明子工程的 module 称号,对应于依靠中的 artifact id
  • children.path:表明子工程对应的相对目录

也便是说,

  • 一个工程下的多个子工程的 group id 有必要相同
  • children.name 有必要和依靠的 artifact id 相同

上述配置文件的作业流程是,

def sourceSwitches = new HashMap<String, SourceSwitch>()
// Load sources configurations.
parseSourcesConfiguration(sourceSwitches)
// Checkout remote sources.
checkoutRemoteSources(sourceSwitches)
// Replace dependencies with sources.
replaceDependenciesWithSources(sourceSwitches)
  • 首要,Gradle 在 setting 阶段解析上述配置文件
  • 然后,依据解析的成果,将翻开源码的工程经过 project 的方法引证到项目中
  • 最终,依据上述配置文件,将项目中的依靠替换为工程引证

2.2 为项目动态增加子工程

如上所述,这儿咱们疏忽掉 json 配置文件解析的环节,直接看拉取最新分支并将其作为子项目增加到项目中的逻辑。该部分代码完结如下,

/** Checkout remote sources if necessary. */
def checkoutRemoteSources(sourceSwitches) {
    def settings = getSettings()
    def rootAbsolutePath = settings.rootDir.absolutePath
    def sourcesRootPath = new File(rootAbsolutePath).parent
    def sourcesDirectory = new File(sourcesRootPath, "open_sources")
    if (!sourcesDirectory.exists()) sourcesDirectory.mkdirs()
    sourceSwitches.forEach { name, sourceSwitch ->
        if (sourceSwitch.open) {
            def sourceDirectory = new File(sourcesDirectory, name)
            if (!sourceDirectory.exists()) {
                logd("clone start [$name] branch [${sourceSwitch.branch}]")
                "git clone -b ${sourceSwitch.branch} ${sourceSwitch.url} ".execute(null, sourcesDirectory).waitFor()
                logd("clone completed [$name] branch [${sourceSwitch.branch}]")
            } else {
                def sb = new StringBuffer()
                "git rev-parse --abbrev-ref HEAD ".execute(null, sourceDirectory).waitForProcessOutput(sb, System.err)
                def currentBranch = sb.toString().trim()
                if (currentBranch != sourceSwitch.branch) {
                    logd("checkout start current branch [${currentBranch}], checkout branch [${sourceSwitch.branch}]")
                    def out = new StringBuffer()
                    "git pull".execute(null, sourceDirectory).waitFor()
                    "git checkout -b ${sourceSwitch.branch} origin/${sourceSwitch.branch}"
                            .execute(null, sourceDirectory).waitForProcessOutput(out, System.err)
                    logd("checkout completed: ${out.toString().trim()}")
                }
            }
            // After checkout sources, include them as subprojects.
            sourceSwitch.children.each { child ->
                settings.include(":${child.name}")
                settings.project(":${child.name}").projectDir = new File(sourcesDirectory, child.path)
            }
        }
    }
}

这儿,我将子项意图源码克隆到 settings.gradle 文件的父目录下的 open_sources 目录下面。这儿当该目录不存在的时分,我会先创立该目录。这儿需求注意的是,我在安排项目目录的时分比较喜欢将项意图子工程放到和主工程相同的方位。所以,上述克隆方法能够保证克隆到的 open_sources 仍然在当前项意图作业目录下。

组件化开发必备:Gradle 依赖切换源码的实践

然后,我对 sourceSwitches,也便是解析的 json 文件数据,进行遍历。这儿会先判断指定的源码是否已经拉下来,假如存在的话就履行 checkout 操作,否则履行 clone 操作。这儿在判断当前分支是否为方针分支的时分运用了 git rev-parse --abbrev-ref HEAD 这个 Git 指令。该指令用来获取当前库房所处的分支。

最终,将源码拉下来之后经过 Settingsinclude() 方法加载指定的子工程,并运用 Settingsproject() 方法指定该子工程的目录。这和咱们在 settings.gradle 文件中增加子工程的方法是相同的,

include ':utils-core', ':utils-ktx'
project(':utils-core').projectDir = new File('../AndroidUtils/utils')
project(':utils-ktx').projectDir = new File('../AndroidUtils/utils-ktx')

2.3 运用子工程替换依靠

动态替换工程依靠运用的是 Gradle 的 ResolutionStrategy 这个功用。或许你对诸如

configurations.all {
  resolutionStrategy.force 'io.reactivex.rxjava2:rxjava:2.1.6'
}

这种写法并不生疏。这儿的 forcedependencySubstitution 相同,都归于 ResolutionStrategy 提供的功用的一部分。只不过这儿的区别是,咱们需求对一切的子项目进行动态更改,因而需求等项目 loaded 完结之后才能履行。

下面是依靠替换的完结逻辑,

/** Replace dependencies with sources. */
def replaceDependenciesWithSources(sourceSwitches) {
    def gradle = settings.gradle
    gradle.projectsLoaded {
        gradle.rootProject.subprojects {
            configurations.all {
                resolutionStrategy.dependencySubstitution {
                    sourceSwitches.forEach { name, sourceSwitch ->
                        sourceSwitch.children.each { child ->
                            substitute module("${sourceSwitch.artifact}:${child.name}") with project(":${child.name}")
                        }
                    }
                }
            }
        }
    }
}

这儿运用 Gradle 的 projectsLoaded 这个点进行 hook,将依靠替换为子工程。

此外,也能够将子工程替换为依靠,比方,

dependencySubstitution {
  substitute module('org.gradle:api') using project(':api')
  substitute project(':util') using module('org.gradle:util:3.0')
}

2.4 注意事项

上述完结方法要求多个子工程的脚本尽可能共同。比方,在 AndroidUtils 的独立工程中,我经过 kotlin_version 这个变量指定 kotlin 的版别,可是在 android-mvvm-and-architecture 这个工程中运用的是 kotlinVersion. 所以,当切换了子工程的源码之后就会发现 kotlin_version 这个变量找不到了。因而,为了完结能够动态切换源码,是需求对 Gradle 脚本做一些调整的。

在我的完结方法中,我并没有将子工程的源码放到主工程的根目录下面,也便是将 open_sources 这个目录放到 appshell 这个目录下面。而是放到和 appshell 同一级别。

组件化开发必备:Gradle 依赖切换源码的实践

这样做的原因是,实践开发过程中,通常咱们会克隆很多库房到 open_sources 这个目录下面(或许之前开发遗留下来的克隆库房)。有些库房尽管咱们封闭了源码依靠,可是因为在 appshell 目录下面,依然会出现在 Android Studio 的工程目录里。而按照上述方法安排目录,我切换了哪个项目等源码,哪个项意图目录会被 Android Studio 加载。其他的因为不在 appshell 目录下面,所以会被 Android Studio 疏忽。这种安排方法能够尽可能削减 Android Studio 加载的文本,提升 Android Studio 响应的速率。

总结

上述是开发过程中替换依靠为源码的“无痕”修正方法。不管在组件化还是非组件化需求开发中都是一种非常有用的开发技巧。按照上述开发开发方法,咱们能够既能开发 android-mvvm-and-architecture 的时分随时随地翻开 AndroidUtils 进行修正,亦可对 AndroidUtil 这个工程独立编译和开发。

源代码参考 android-mvvm-and-architecture 项目(当前是 feature-3.0 分支)的 AppShell 下面的 sources.gradle 文件。