Android gradle dependency tree change(依赖树变化)监控实现

文章目录

    • 前言
    • 基本原理
    • 执行流程
    • diff 报告
      • 不同分支 merge 过来的 diff 报告
      • 同个分支产生的 merge 报告
      • 同个分支提交的 diff 报告
    • 具体实现原理
      • 我们需要监控怎样的 Dendenpency 变化
      • 怎样获取 dependency Tree
        • `project.configurations` 方式
        • ./gradlew dependencies
        • AsciiDependencyReportRenderer
        • 方案选择
      • 怎样对 dependency Tree 进行 diff 计算
        • 传统 diff 方案
        • 自定义的 diff 方案
      • 如何找到一个基准点,进行 diff 计算
      • 怎样集合 Gialab CI 进行计算
    • 总结
    • 参考文章

前言

这篇文章,其实在一年之前的时候就已经写好了。当时是在公司内部分享的,作为一个监控框架。当时是想着过一段时间之后,分享到技术论坛上面的,没想到计划赶不上变化,过完国庆被裁了

当时忙着找工作,就一直没有更新了,放在笔记里面吃灰

最近,发现好久没有分享技术文章了,从笔记里面找了一下,就拿来分享了

在项目开发中,会有很多第三方依赖,通过 gradle 引入进来的。比如 androidxDesignVersion、androidxSupportVersion、 rxjava2Version、 okhttpVersion 等第三方库。有时候第三方库改到了或者升级了,我们并不能及时发现,往往需要等到出问题的时候,去排查的时候,才发现是某个依赖版本改动导致的。

这时候其实是有点晚了,如果能够提前暴露,那么我们能够大大地减少风险,因此我们希望能够监控起来。

基本原理

  1. 代码 merge 到 dev 分支的时候,借助 gitlab ci,促发 gradle task 任务,自动分析 dependency 链表
  2. 对比上一次打包的 dependency 链表,如果发现变更了,会通过 机器人进行通知。并附上最新的 commit,提交作者信息,需要 author 确认一下

执行流程

目前主要对 dev 分支进行监控,以下几种场景会促发 diff 检查

  • MR 合并进 dev 分支的时候
  • 在 dev 分支直接提交代码的时候

Android gradle dependency tree change(依赖树变化)监控实现_第1张图片

diff 报告

diff 报告主要包括以下几种信息

  • 作者,当前 commitId 的 author
  • branch 分支名
  • commitId 当前的 commitId, baseCommitId:基准 id
  • 变动依赖,这里最多显示 6 行,超过会截断,具体变动可以见详情
  • 提交:如果是 MR 合并进来的,会显示 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

同个分支产生的 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

我们主要讲述以下几点

  • 我们需要监控怎样的 Dendenpency 变化
  • 怎样获取 dependency Tree
  • dependency Tree 怎样做 diff
  • 如何找到基准点,进行 diff 计算
  • 怎样结合 CI 进行计算

具体实现原理

我们需要监控怎样的 Dendenpency 变化

众所周知,Android 的 Dependency 是通过 gradle 进行配置的,如果我们在 build.gradle 下面配置了这样,证明了我们依赖 recyclerview 这个库。

dependencies {
    implementation androidx.recyclerview:recyclerview:1.1.0 ”
}

那一行代码会给我们的 Dendenpency 带来怎样的变化呢?

有人说,它是新增了 recyclerview 这个库。

这个说法对嘛?

不全对。

因为 gradle 依赖默认是有传递性的。他还会同时引入 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 (*)
  1. 如果项目当中当前没有这些库的,会同时导入这些库。
  2. 如果项目中有这些库了,库的版本比较低,会升级到相应的版本。比如 collection 会从 1.0.0 升级到 1.1.0

然而这些情况就是我们往往所忽略的,即使有代码 review,有时候也会漏了。即使 review 待了,可能下意识也只以为只引入了这个库,却很难看到它背后的变化

而这些如果带到线上去,有时候会发生一些难以预测的结果,因此,我们需要有专门的手段来监控这些变化。能够监测到整条链路的变化,而不仅仅只是 implementation androidx.recyclerview:recyclerview:1.1.0 ” 这行代码的变化

至于如果依赖的传递性,可以通过 transitiveexclude 等用法做到。 可以看这些文章,这里不再一一展开。

解决 Android Gradle 依赖的各种版本问题

build.gradle管理依赖的版本(传递(transitive)\排除(exclude)\强制(force)\动态版本(+))

怎样获取 dependency Tree

获取 dependency Tree 的话,有多种方式

  1. 通过 project.configurations 这种方式获取
  2. 通过 gradlew :app:dependencies task
  3. 通过 AsciiDependencyReportRenderer 获取,需要适配不同版本的 gradle 版本
project.configurations 方式

通过这种方式获取的,他是能够获取到所有的 dependencies,但是并不能看到 dependencies 的树形关系。

Android gradle dependency tree change(依赖树变化)监控实现_第2张图片

伪代码如下

        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 等等

事实上,我们只关心打进 APK 包里面的 dependencies。因此我们可以指定更详细的 configuration 。即

gradlew :app:dependencies --configuration releaseRuntimeClasspath

这样,就只会输出 Release 包 runtimeClasspath 相关的东西。

RuntimeClasspath 跟我们常用的 implementation,关系大概如下

Android gradle dependency tree change(依赖树变化)监控实现_第3张图片

在输出的 dependencies tree 报告中,我们看到的格式一般是这样的

Android gradle dependency tree change(依赖树变化)监控实现_第4张图片

** 这里有几个格式需要说明一下**

  • x.x.x (*), 比如图中的 4.2.2(*), 该依赖已经有了,将不再重复依赖,
  • x.x.x -> x.x.x 该依赖的版本被箭头所指的版本代替
  • x.x.x -> x.x.x(*) 该依赖的版本被箭头所指的版本代替,并且该依赖已经有了,不再重复依赖
AsciiDependencyReportRenderer

AsciiDependencyReportRenderer 这个东东,在不同的 gradle 版本有不同的差异,需要适配一下。

如果要这种方案,建议将某个版本的代码剥离出来,伪代码一般如下,单独集成一个库。

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 的树形关系。

第二种方案 ./gradlew dependencies 的优点是简单,直接采用 gradle 原生 Task,输出特定格式的文本。然后根据规律将所有的 dependency tree 提出出来。

可能有人担心 ./gradlew dependencies 的输出格式会变化。

其实还好,看了几个 gradle 版本的输出格式,基本都是一样的。

第三种方AsciiDependencyReportRenderer 的优点是可定制性高,缺点是麻烦,需要适配不同版本的 gradle。

最终我选择的方案是方案二

怎样对 dependency Tree 进行 diff 计算

传统 diff 方案

可能很多人想到的方案是使用 Git diff 进行 diff 计算。但是这种方式有局限性。

  • 当有多个修改的时候,key -value 可能无法一一对应。
  • 他的 diff 类型 add、remove、 change 并不能一一对应我们 dependency add、remove、 change 的类型。

这无法达到我们想要的结果。因此,我们需要整合自己的 diff 算法。

自定义的 diff 方案

这里的方案是借鉴了 JakeWharton 大神的方案,在其基础之上进行了改造。

原理大概如下

  1. 分别计算当前,上一次的 dependency tree,用 Set 储存,分别表示为 oldPaths,newPaths
  2. 接着根据 oldPaths 和 newPaths 计算出 removedTree, addedTree, changedTree
  3. 最后,根据 removedTree, addedTree 计算出 diff

Android gradle dependency tree change(依赖树变化)监控实现_第5张图片

第一步

对于这里的依赖,我们会使用 Set> 的数据结构储存
Android gradle dependency tree change(依赖树变化)监控实现_第6张图片

转换之后的数据结构

Android gradle dependency tree change(依赖树变化)监控实现_第7张图片

这样的好处就是可以看到每一个 dependency 的全路径,如果 dependency 的全路径不一样,那么可以 diff 出来。

第二步 计算 remove 树 和 add 树

有了第一步的基础,其实很简单,直接调用 kotlin 的扩展方法 Set.minus

如何找到一个基准点,进行 diff 计算

其实,这个说到底,就是找到上一个 commit 提交的 diff 文件。

  1. 看是不是 MR,如果是 MR,我们应该找到 MR 合并前的一个 commit
  2. 不是 MR 合并进来的,我们直接找到上一个 commit 即可

因此,我们可以借助 git 命令来处理。对于 merge request,目前主要有几种情况会产生 merge request。

  • 直接 MR 合并进来的,这时候 parent 会产生两个点,我们去 parent[0] 即可
  • 当前本地分支落后远程分支, 且 local 分支有 commit 的时候,pull 或者 push 的时候,会产生一个 merge 节点,这时候 parent 会产生两个点,我们去 parent[1] 即可

原理图如下:

Android gradle dependency tree change(依赖树变化)监控实现_第8张图片

怎样集合 Gialab CI 进行计算

Gialab push 或者 merge 的时候,我们需要感知到,接着执行特定的 task,进行计算。 每个公司的 CI 可能不太一样,具体可以修改一下

gradlew :{appName}:checkDepDiff

总结

dependency diff 监控的原理其实不难,主要是涉及到挺多方面的,有兴趣的可以看一下。如果觉得对你有所帮助的话,希望可以一键三连。

参考文章

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/

你可能感兴趣的:(android,进阶之路,android,gradle,gradle依赖监控,dependency,tree)