Gradle 依賴切換源碼的實踐

語言: CN / TW / HK

最近,因為開發的時候經改動依賴的庫,所以,我想對 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 配置文件,

json [ { "name": "AndroidUtils", "url": "[email protected]: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 相同

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

```groovy def sourceSwitches = new HashMap()

// Load sources configurations. parseSourcesConfiguration(sourceSwitches)

// Checkout remote sources. checkoutRemoteSources(sourceSwitches)

// Replace dependencies with sources. replaceDependenciesWithSources(sourceSwitches) ```

  • 首先,Gradle 在 setting 階段解析上述配置文件
  • 然後,根據解析的結果,將打開源碼的工程通過 project 的形式引用到項目中
  • 最後,根據上述配置文件,將項目中的依賴替換為工程引用

2.2 為項目動態添加子工程

如上所述,這裏我們忽略掉 json 配置文件解析的環節,直接看拉取最新分支並將其作為子項目添加到項目中的邏輯。該部分代碼實現如下,

groovy /** 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 仍然在當前項目的工作目錄下。

工程目錄示例

然後,我對 sourceSwitches,也就是解析的 json 文件數據,進行遍歷。這裏會先判斷指定的源碼是否已經拉下來,如果存在的話就執行 checkout 操作,否則執行 clone 操作。這裏在判斷當前分支是否為目標分支的時候使用了 git rev-parse --abbrev-ref HEAD 這個 Git 指令。該指令用來獲取當前倉庫所處的分支。

最後,將源碼拉下來之後通過 Settingsinclude() 方法加載指定的子工程,並使用 Settingsproject() 方法指定該子工程的目錄。這和我們在 settings.gradle 文件中添加子工程的方式是相同的,

groovy 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 這個功能。也許你對諸如

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

這種寫法並不陌生。這裏的 forcedependencySubstitution 一樣,都屬於 ResolutionStrategy 提供的功能的一部分。只不過這裏的區別是,我們需要對所有的子項目進行動態更改,因此需要等項目 loaded 完成之後才能執行。

下面是依賴替換的實現邏輯,

groovy /** 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,將依賴替換為子工程。

此外,也可以將子工程替換為依賴,比如,

groovy 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 同一級別。

工程目錄示例

這樣做的原因是,實際開發過程中,通常我們會克隆很多倉庫到 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 文件。