首页 > 其他分享 >Android gradle dependency tree change(依赖树变化)监控实现,sdk version 变化一目了然

Android gradle dependency tree change(依赖树变化)监控实现,sdk version 变化一目了然

时间:2024-01-17 09:55:36浏览次数:36  
标签:依赖 tree gradle dependency dependencies 版本 diff

@

目录

前言

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

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

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

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

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

在这里插入图片描述

基本原理

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

执行流程

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

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

img

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

伪代码如下

        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,关系大概如下

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

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

  • 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<List> 储存,分别表示为 oldPaths,newPaths
  2. 接着根据 oldPaths 和 newPaths 计算出 removedTree, addedTree, changedTree
  3. 最后,根据 removedTree, addedTree 计算出 diff

第一步

对于这里的依赖,我们会使用 Set<List<String>> 的数据结构储存

转换之后的数据结构

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

第二步 计算 remove 树 和 add 树

有了第一步的基础,其实很简单,直接调用 kotlin 的扩展方法 Set<T>.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] 即可

原理图如下:

怎样集合 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/

标签:依赖,tree,gradle,dependency,dependencies,版本,diff
From: https://www.cnblogs.com/gdutxiaoxu/p/17969165

相关文章

  • SourceTree提交快捷键(Ctrl+Enter)无效问题问题
    之前用SourceTree,Ctrl+Enter快速commit。最近更新版本到3.4.16后,突然发现不能用了。更新大概是跨了几个小版本。。。在atlassian论坛里发现,这个果然就是更新的变更(我不是一个人.jpg)。目前大概有两个方法解决:回退到3.4.15或之前的版本。从旧的安装包(3.4.15)里提取两个dll进......
  • JMeter 源码解读 - HashTree
    背景:在JMeter中,HashTree是一种用于组织和管理测试计划元素的数据结构。它是一个基于LinkedHashMap的特殊实现,提供了一种层次结构的方式来存储和表示测试计划的各个组件。HashTree的特点如下:层次结构:HashTree使用树状结构来组织测试计划元素。每个节点都可以包含子节点......
  • 聊一聊为什么我要整合Microsoft.Extensions.DependencyInjection和Castle.Core
    前言如果用到动态代理,大家可能会有几种选择,排到前列的是Autofac+Castle、AspectCore和DoraInterception,我将从我当时研究的经历,以及我遇到的场景,为大家展示下聊一聊我为什么要费时费力的整合Microsoft.Extensions.DependencyInjection和Castle.Core当时遇到的场景直接上源码......
  • MYSQL的B+Tree索引树高度如何计算
    前一段被问到一个平时没有关注到有关于MYSQL索引相关的问题点,被问到一个表有3000万记录,假如有一列占8位字节的字段,根据这一列建索引的话索引树的高度是多少?这一问当时就被问蒙了,平时这也只关注MySQL索引一般都是都是用B+Tree来存储维护索引的,还有一些复合索引的最左匹配原则等等,......
  • POJ1635subway tree system
    在扫描过程中一旦扫描到一个子串01数量相等了,这个时候肯定是已经递归回到根节点了,因为从根节点下去的一步操作给了一个0,而这个0一定要从这条边回到根节点才能产生一个1与其匹配(这个1不可能来自其他边的回溯,因为其他边的回溯的前提就是之前从这条边下去了,就会产生一个0,,这个0就要......
  • dependency-check部署
    Linux安装部署(Ubuntu)WEB-UI项目地址,作者已经很久没更新了:https://github.com/he1m4n6a/dcweb从github上拉取下来gitclonehttps://github.com/he1m4n6a/dcweb.git之后本地机器会出现“dcweb”的文件夹:然后使用python2安装requirements.txt中的包。其中Django的版本最好......
  • 【树上启发式合并】CF1709E XOR Tree
    XORTree\(\mathtt{TAGS}\):树上启发式合并+异或+贪心\(\mathtt{ESTIMATION}\):非常好的启发式合并题目First.如何去除\(0\)路径对于一条路径\(u\tov\),要使其不为\(0\)肯定是将\(\mathtt{LCA}(u,v)\)变为\(2^{30+x}\)最好,这样异或值的第\(30+x\)位一......
  • 聊一聊如何整合Microsoft.Extensions.DependencyInjection和Castle.Core(完结篇)
    聊一聊如何整合Microsoft.Extensions.DependencyInjection和Castle.Core(完结篇) 合集-聊一聊如何整合Microsoft默认的Ioc容器和Castle.Core(4) 1.聊一聊如何整合Microsoft.Extensions.DependencyInjection和Castle.Core(二)01-122.聊一聊如何结合Microsoft.Extension......
  • 聊一聊如何整合Microsoft.Extensions.DependencyInjection和Castle.Core(三)
    聊一聊如何整合Microsoft.Extensions.DependencyInjection和Castle.Core(三) 合集-聊一聊如何整合Microsoft默认的Ioc容器和Castle.Core(4) 1.聊一聊如何整合Microsoft.Extensions.DependencyInjection和Castle.Core(二)01-122.聊一聊如何结合Microsoft.Extensions.De......
  • 聊一聊如何整合Microsoft.Extensions.DependencyInjection和Castle.Core(二)
    聊一聊如何整合Microsoft.Extensions.DependencyInjection和Castle.Core(二) 合集-聊一聊如何整合Microsoft默认的Ioc容器和Castle.Core(4) 1.整合Microsoft.Extensions.DependencyInjection和Castle.Core(二)01-122.聊一聊如何结合Microsoft.Extensions.DependenyI......