Android gradle dependency tree change(依赖树变化)监控实现,sdk version 变化一目了然
@
目錄- 前言
- 基本原理
- 執(zhí)行流程
-
diff 報告
- 不同分支 merge 過來的 diff 報告
- 同個分支產(chǎn)生的 merge 報告
- 同個分支提交的 diff 報告
-
具體實(shí)現(xiàn)原理
- 我們需要監(jiān)控怎樣的 Dendenpency 變化
-
怎樣獲取 dependency Tree
project.configurations方式- ./gradlew dependencies
- AsciiDependencyReportRenderer
- 方案選擇
-
怎樣對 dependency Tree 進(jìn)行 diff 計算
- 傳統(tǒng) diff 方案
- 自定義的 diff 方案
- 如何找到一個基準(zhǔn)點(diǎn),進(jìn)行 diff 計算
- 怎樣集合 Gialab CI 進(jìn)行計算
- 總結(jié)
- 參考文章
前言
這篇文章,其實(shí)在一年之前的時候就已經(jīng)寫好了。當(dāng)時是在公司內(nèi)部分享的,作為一個監(jiān)控框架。當(dāng)時是想著過一段時間之后,分享到技術(shù)論壇上面的,沒想到計劃趕不上變化,過完國慶被裁了。
當(dāng)時忙著找工作,就一直沒有更新了,放在筆記里面吃灰。
最近,發(fā)現(xiàn)好久沒有分享技術(shù)文章了,從筆記里面找了一下,就拿來分享了。
在項(xiàng)目開發(fā)中,會有很多第三方依賴,通過 gradle 引入進(jìn)來的。比如 androidxDesignVersion、androidxSupportVersion、 rxjava2Version、 okhttpVersion 等第三方庫。有時候第三方庫改到了或者升級了,我們并不能及時發(fā)現(xiàn),往往需要等到出問題的時候,去排查的時候,才發(fā)現(xiàn)是某個依賴版本改動導(dǎo)致的。
這時候其實(shí)是有點(diǎn)晚了,如果能夠提前暴露,那么我們能夠大大地減少風(fēng)險,因此我們希望能夠監(jiān)控起來。
基本原理
- 代碼 merge 到 dev 分支的時候,借助 gitlab ci,促發(fā) gradle task 任務(wù),自動分析 dependency 鏈表
- 對比上一次打包的 dependency 鏈表,如果發(fā)現(xiàn)變更了,會通過 機(jī)器人進(jìn)行通知。并附上最新的 commit,提交作者信息,需要 author 確認(rèn)一下
執(zhí)行流程
目前主要對 dev 分支進(jìn)行監(jiān)控,以下幾種場景會促發(fā) diff 檢查
- MR 合并進(jìn) dev 分支的時候
- 在 dev 分支直接提交代碼的時候
diff 報告
diff 報告主要包括以下幾種信息
- 作者,當(dāng)前 commitId 的 author
- branch 分支名
- commitId 當(dāng)前的 commitId, baseCommitId:基準(zhǔn) id
- 變動依賴,這里最多顯示 6 行,超過會截斷,具體變動可以見詳情
- 提交:如果是 MR 合并進(jìn)來的,會顯示 MR 鏈接,否則,會顯示 commit 鏈接
不同分支 merge 過來的 diff 報告
檢測到 Dependency 變化
分支: 573029_test
作者: 徐俊
commitId: 4844590b baseCommitId: bed4cb64
變動依賴:
+\--- project :component-matrix
+ \--- com.google.code.gson:gson:2.8.2 -> 2.8.9
詳情: {url}
提交:{url}/merge_requests/4425/diffs
同個分支產(chǎn)生的 merge 報告
檢測到 Dependency 變化
分支: 573029_dep_diff
作者: xujun
commitId: 16145365 baseCommitId: 4844590b
變動依賴:
+\--- project :component-matrix
+ \--- com.squareup.retrofit2:converter-gson:2.4.0 (*)
詳情: {url}
提交: {url)/commit/16145365
同個分支提交的 diff 報告
檢測到 Dependency 變化
分支: 573029_dep_diff
作者: xujun
commitId: 19f22516 baseCommitId: 8c90d512
變動依賴:
+\--- project :component-tcpcache
+ \--- com.google.code.gson:gson:2.8.2 -> 2.8.9
詳情: {url}
提交: {url)/commit/16145365
我們主要講述以下幾點(diǎn)
- 我們需要監(jiān)控怎樣的 Dendenpency 變化
- 怎樣獲取 dependency Tree
- dependency Tree 怎樣做 diff
- 如何找到基準(zhǔn)點(diǎn),進(jìn)行 diff 計算
- 怎樣結(jié)合 CI 進(jìn)行計算
具體實(shí)現(xiàn)原理
我們需要監(jiān)控怎樣的 Dendenpency 變化
眾所周知,Android 的 Dependency 是通過 gradle 進(jìn)行配置的,如果我們在 build.gradle 下面配置了這樣,證明了我們依賴 recyclerview 這個庫。
dependencies {
implementation androidx.recyclerview:recyclerview:1.1.0 ”
}
那一行代碼會給我們的 Dendenpency 帶來怎樣的變化呢?
有人說,它是新增了 recyclerview 這個庫。
這個說法對嘛?
不全對。
因?yàn)?gradle 依賴默認(rèn)是有傳遞性的。他還會同時引入 recyclerview 自身所依賴的庫。
+--- androidx.recyclerview:recyclerview:1.1.0
| +--- androidx.annotation:annotation:1.1.0
| +--- androidx.core:core:1.1.0
| +--- androidx.customview:customview:1.1.0
| \--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
- 如果項(xiàng)目當(dāng)中當(dāng)前沒有這些庫的,會同時導(dǎo)入這些庫。
- 如果項(xiàng)目中有這些庫了,庫的版本比較低,會升級到相應(yīng)的版本。比如 collection 會從 1.0.0 升級到 1.1.0
然而這些情況就是我們往往所忽略的,即使有代碼 review,有時候也會漏了。即使 review 待了,可能下意識也只以為只引入了這個庫,卻很難看到它背后的變化。
而這些如果帶到線上去,有時候會發(fā)生一些難以預(yù)測的結(jié)果,因此,我們需要有專門的手段來監(jiān)控這些變化。能夠監(jiān)測到整條鏈路的變化,而不僅僅只是 implementation androidx.recyclerview:recyclerview:1.1.0 ” 這行代碼的變化
至于如果依賴的傳遞性,可以通過 transitive、exclude 等用法做到。 可以看這些文章,這里不再一一展開。
解決 Android Gradle 依賴的各種版本問題
build.gradle管理依賴的版本(傳遞(transitive)\排除(exclude)\強(qiáng)制(force)\動態(tài)版本(+))
怎樣獲取 dependency Tree
獲取 dependency Tree 的話,有多種方式
- 通過
project.configurations這種方式獲取 - 通過
gradlew :app:dependenciestask - 通過
AsciiDependencyReportRenderer獲取,需要適配不同版本的 gradle 版本
project.configurations 方式
通過這種方式獲取的,他是能夠獲取到所有的 dependencies,但是并不能看到 dependencies 的樹形關(guān)系。
偽代碼如下
def configuration = project.configurations.getByName("debugCompileClasspath")
configuration.resolvedConfiguration.lenientConfiguration.allModuleDependencies.each {
def identifer = it.module.id
depList.add(identifer)
}
./gradlew dependencies
./gradlew dependencies 會輸出所有 configuration 的 Dependcency Tree。包括 testDebugImplementation、testDebugProvided、testDebugRuntimeOnly 等等
事實(shí)上,我們只關(guān)心打進(jìn) APK 包里面的 dependencies。因此我們可以指定更詳細(xì)的 configuration 。即
gradlew :app:dependencies --configuration releaseRuntimeClasspath
這樣,就只會輸出 Release 包 runtimeClasspath 相關(guān)的東西。
RuntimeClasspath 跟我們常用的 implementation,關(guān)系大概如下
在輸出的 dependencies tree 報告中,我們看到的格式一般是這樣的
** 這里有幾個格式需要說明一下**
- x.x.x (*), 比如圖中的 4.2.2(*), 該依賴已經(jīng)有了,將不再重復(fù)依賴,
- x.x.x -> x.x.x 該依賴的版本被箭頭所指的版本代替
- x.x.x -> x.x.x(*) 該依賴的版本被箭頭所指的版本代替,并且該依賴已經(jīng)有了,不再重復(fù)依賴
AsciiDependencyReportRenderer
AsciiDependencyReportRenderer 這個東東,在不同的 gradle 版本有不同的差異,需要適配一下。
如果要這種方案,建議將某個版本的代碼剝離出來,偽代碼一般如下,單獨(dú)集成一個庫。
project.afterEvaluate {
Log.i(TAG, "afterEvaluate")
val renderer = AsciiDependencyReportRenderer()
val sb = StringBuilder()
val f = StreamingStyledTextOutputFactory(sb)
renderer.setOutput(f.create(javaClass, LogLevel.INFO))
val projectDetails = ProjectDetails.of(project)
renderer.startProject(projectDetails)
// sort all dependencies
val configuration: org.gradle.api.artifacts.Configuration =
project.configurations.getByName("releaseRuntimeClasspath")
renderer.startConfiguration(configuration)
renderer.render(configuration)
renderer.completeConfiguration(configuration)
// finish the whole processing
renderer.completeProject(projectDetails)
val textOutput = renderer.textOutput
textOutput.println()
Log.i(TAG, "end sb is $sb")
}
方案選擇
從上面闡述可知,第一種方案 project.configurations, 通過這種方式獲取的,他是能夠獲取到所有的 dependencies,但是并不能看到 dependencies 的樹形關(guān)系。
第二種方案 ./gradlew dependencies 的優(yōu)點(diǎn)是簡單,直接采用 gradle 原生 Task,輸出特定格式的文本。然后根據(jù)規(guī)律將所有的 dependency tree 提出出來。
可能有人擔(dān)心 ./gradlew dependencies 的輸出格式會變化。
其實(shí)還好,看了幾個 gradle 版本的輸出格式,基本都是一樣的。
第三種方案 AsciiDependencyReportRenderer 的優(yōu)點(diǎn)是可定制性高,缺點(diǎn)是麻煩,需要適配不同版本的 gradle。
最終我選擇的方案是方案二。
怎樣對 dependency Tree 進(jìn)行 diff 計算
傳統(tǒng) diff 方案
可能很多人想到的方案是使用 Git diff 進(jìn)行 diff 計算。但是這種方式有局限性。
- 當(dāng)有多個修改的時候,key -value 可能無法一一對應(yīng)。
- 他的 diff 類型 add、remove、 change 并不能一一對應(yīng)我們 dependency add、remove、 change 的類型。
這無法達(dá)到我們想要的結(jié)果。因此,我們需要整合自己的 diff 算法。
自定義的 diff 方案
這里的方案是借鑒了 JakeWharton 大神的方案,在其基礎(chǔ)之上進(jìn)行了改造。
原理大概如下
- 分別計算當(dāng)前,上一次的 dependency tree,用 Set<List
> 儲存,分別表示為 oldPaths,newPaths - 接著根據(jù) oldPaths 和 newPaths 計算出 removedTree, addedTree, changedTree
- 最后,根據(jù) removedTree, addedTree 計算出 diff
第一步
對于這里的依賴,我們會使用 Set<List<String>> 的數(shù)據(jù)結(jié)構(gòu)儲存
轉(zhuǎn)換之后的數(shù)據(jù)結(jié)構(gòu)
這樣的好處就是可以看到每一個 dependency 的全路徑,如果 dependency 的全路徑不一樣,那么可以 diff 出來。
第二步 計算 remove 樹 和 add 樹
有了第一步的基礎(chǔ),其實(shí)很簡單,直接調(diào)用 kotlin 的擴(kuò)展方法 Set<T>.minus
如何找到一個基準(zhǔn)點(diǎn),進(jìn)行 diff 計算
其實(shí),這個說到底,就是找到上一個 commit 提交的 diff 文件。
- 看是不是 MR,如果是 MR,我們應(yīng)該找到 MR 合并前的一個 commit
- 不是 MR 合并進(jìn)來的,我們直接找到上一個 commit 即可
因此,我們可以借助 git 命令來處理。對于 merge request,目前主要有幾種情況會產(chǎn)生 merge request。
- 直接 MR 合并進(jìn)來的,這時候 parent 會產(chǎn)生兩個點(diǎn),我們?nèi)?parent[0] 即可
- 當(dāng)前本地分支落后遠(yuǎn)程分支, 且 local 分支有 commit 的時候,pull 或者 push 的時候,會產(chǎn)生一個 merge 節(jié)點(diǎn),這時候 parent 會產(chǎn)生兩個點(diǎn),我們?nèi)?parent[1] 即可
原理圖如下:
怎樣集合 Gialab CI 進(jìn)行計算
Gialab push 或者 merge 的時候,我們需要感知到,接著執(zhí)行特定的 task,進(jìn)行計算。 每個公司的 CI 可能不太一樣,具體可以修改一下
gradlew :{appName}:checkDepDiff
總結(jié)
dependency diff 監(jiān)控的原理其實(shí)不難,主要是涉及到挺多方面的,有興趣的可以看一下。如果覺得對你有所幫助的話,希望可以一鍵三連。
參考文章
https://wajahatkarim.com/2020/03/gradle-dependency-tree/
https://tomgregory.com/gradle-dependency-tree/
https://github.com/jfrog/gradle-dep-tree
http://muydev.top/2018/08/21/Analyze-Android-Dependency-Tree/
總結(jié)
以上是生活随笔為你收集整理的Android gradle dependency tree change(依赖树变化)监控实现,sdk version 变化一目了然的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: utu2440 gdbserver 搭建
- 下一篇: 如何关掉硬盘自检