Android gradle dependency tree change(依赖树变化)监控实现,sdk version 变化一目了然
@
前言
这篇文章,其实在一年之前的时候就已经写好了。当时是在公司内部分享的,作为一个监控框架。当时是想着过一段时间之后,分享到技术论坛上面的,没想到计划赶不上变化,过完国庆被裁了。
当时忙着找工作,就一直没有更新了,放在笔记里面吃灰。
最近,发现好久没有分享技术文章了,从笔记里面找了一下,就拿来分享了。
在项目开发中,会有很多第三方依赖,通过 gradle 引入进来的。比如 androidxDesignVersion、androidxSupportVersion、 rxjava2Version、 okhttpVersion 等第三方库。有时候第三方库改到了或者升级了,我们并不能及时发现,往往需要等到出问题的时候,去排查的时候,才发现是某个依赖版本改动导致的。
这时候其实是有点晚了,如果能够提前暴露,那么我们能够大大地减少风险,因此我们希望能够监控起来。

基本原理
- 代码 merge 到 dev 分支的时候,借助 gitlab ci,促发 gradle task 任务,自动分析 dependency 链表
- 对比上一次打包的 dependency 链表,如果发现变更了,会通过 机器人进行通知。并附上最新的 commit,提交作者信息,需要 author 确认一下
执行流程
目前主要对 dev 分支进行监控,以下几种场景会促发 diff 检查
- MR 合并进 dev 分支的时候
- 在 dev 分支直接提交代码的时候

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 (*)
- 如果项目当中当前没有这些库的,会同时导入这些库。
- 如果项目中有这些库了,库的版本比较低,会升级到相应的版本。比如 collection 会从 1.0.0 升级到 1.1.0
然而这些情况就是我们往往所忽略的,即使有代码 review,有时候也会漏了。即使 review 待了,可能下意识也只以为只引入了这个库,却很难看到它背后的变化。
而这些如果带到线上去,有时候会发生一些难以预测的结果,因此,我们需要有专门的手段来监控这些变化。能够监测到整条链路的变化,而不仅仅只是 implementation androidx.recyclerview:recyclerview:1.1.0 ” 这行代码的变化
至于如果依赖的传递性,可以通过 transitive、exclude 等用法做到。 可以看这些文章,这里不再一一展开。
build.gradle管理依赖的版本(传递(transitive)\排除(exclude)\强制(force)\动态版本(+))
怎样获取 dependency Tree
获取 dependency Tree 的话,有多种方式
- 通过
project.configurations这种方式获取 - 通过
gradlew :app:dependenciestask - 通过
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 大神的方案,在其基础之上进行了改造。
原理大概如下
- 分别计算当前,上一次的 dependency tree,用 Set<List> 储存,分别表示为 oldPaths,newPaths
- 接着根据 oldPaths 和 newPaths 计算出 removedTree, addedTree, changedTree
- 最后,根据 removedTree, addedTree 计算出 diff

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

转换之后的数据结构

这样的好处就是可以看到每一个 dependency 的全路径,如果 dependency 的全路径不一样,那么可以 diff 出来。
第二步 计算 remove 树 和 add 树
有了第一步的基础,其实很简单,直接调用 kotlin 的扩展方法 Set<T>.minus
如何找到一个基准点,进行 diff 计算
其实,这个说到底,就是找到上一个 commit 提交的 diff 文件。
- 看是不是 MR,如果是 MR,我们应该找到 MR 合并前的一个 commit
- 不是 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/
Android gradle dependency tree change(依赖树变化)监控实现,sdk version 变化一目了然的更多相关文章
- Gradle 翻译 build dependencies 依赖 MD
Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...
- 读书笔记--Android Gradle权威指南(下)
前言 最近看了一本书<Android Gradle 权威指南>,收获挺多,就想着来记录一些读书笔记,方便后续查阅. 本篇内容是基于上一篇:读书笔记--Android Gradle权威指南( ...
- 读书笔记--Android Gradle权威指南(上)
本篇文章已授权微信公众号 dasu_Android(大苏)独家发布 最近看了一本书<Android Gradle 权威指南>,对于 Gradle 理解又更深了,但不想过段时间就又忘光了,所 ...
- Android Gradle 依赖配置:implementation & api
背景: Android Gradle plugin 3.0开始(对应Gradle版本 4.1及以上),原有的依赖配置类型compile已经被废弃,开始使用implementation.api和anno ...
- 查看maven项目的依赖关系 mvn dependency:tree
maven-dependency-plugin最大的用途是帮助分析项目依赖,dependency:list能够列出项目最终解析到的依赖列表,dependency:tree能进一步的描绘项目依赖树,de ...
- Android Gradle 隐形依赖的奇怪案例
相信 Android 开发者都有在 Android Studio 中升级 compileSdkVersion 的经历,这个时候如果你使用了 support 包,并同时升级,那么可能会出现一个错误提示. ...
- Android 查看项目依赖树的四种方式
Android 查看项目依赖树的四种方式: 方式一: ./gradlew 模块名:dependencies //查看单独模块的依赖 ./gradlew :app:dependencies --conf ...
- Android Gradle 依赖方式
Android Gradle 依赖方式有以下6种: Compile compile是对所有的build type以及favlors都会参与编译并且打包到最终的apk文件中. Provided Prov ...
- maven 查看依赖树结构命令mvn dependency:tree
使用maven 管理项目的依赖,可以使用如下命令查看依赖树结构: mvn dependency:tree 如下图是使用idea的终端执行命令的局部图: 也可以使用如下命令将输出定向到某个文件,这样就可 ...
- Android Gradle 技巧之一: Build Variant 相关
Build Variant android gradle 插件,允许对最终的包以多个维度进行组合. BuildVariant = ProductFlavor x BuildType 两个维度 最常见的 ...
随机推荐
- Util应用框架基础(六) - 日志记录(四) - 写入 Exceptionless
本文是Util应用框架日志记录的第四篇,介绍安装和写入 Exceptionless 日志系统的配置方法. Exceptionless 是一个日志管理系统,使用 Asp.Net Core 开发,比 Se ...
- 反转字符串里的单词(leetcode 4.10每日打卡)
给定一个字符串,逐个翻转字符串中的每个单词. 示例 1: 输入: "the sky is blue"输出: "blue is sky the" 示例 2: ...
- 为什么 Django 后台管理系统那么“丑”?
哈喽大家好,我是咸鱼 相信使用过 Django 的小伙伴都知道 Django 有一个默认的后台管理系统--Django Admin 它的 UI 很多年都没有发生过变化,现在看来显得有些"过时 ...
- 学生开发者勇担青年使命,用AI守护少数人的“视界”
本文分享自华为云社区<[先锋开发者云上说]学生开发者勇担青年使命,用AI守护少数人的"视界">,作者:华为云社区精选 . 青年动人之处,在于他们的勇气,和非凡的创造探索 ...
- Spring Boot 关闭 Actuator ,满足安全工具扫描
应用被安全工具,扫描出漏洞信息 [MSS]SpringBoot Actuator敏感接口未授权访问漏洞(Actuator)事件发现通告: 发现时间:2023-11-25 19:47:17 攻击时间:2 ...
- # [AI]多模态聚类能力助力AI完成自主意识测试
引言 探讨人工智能是否能形成自我意识,是一个当前AI领域一个重要而又复杂的问题.随着深度学习和强化学习技术的不断进步,计算机在视觉识别.语音识别和控制机器人等方面都已取得长足的进展,模拟和超越人类的一 ...
- 吉特日化MES配料工艺参数标准版-第二版
作者:情缘 出处:http://www.cnblogs.com/qingyuan/ 关于作者:从事仓库,生产软件方面的开发,在项目管理以及企业经营方面寻求发展之路 版权声明:本文版权归作者和博客园共有 ...
- [转载] Winform WebBrowser 使用 Edge 内核
原文地址 C# 设置 WebBrowser 使用 Edge 内核_c# webbrowser 内核 - CSDN 博客 原文内容 1. 问题描述 用 C# 写了一个小工具, 需要显示网页上的内容, 但 ...
- 算法与数据结构——kpm算法
- Javascript Ajax总结——XMLHttpRequest对象
Ajax技术能向服务器异步请求额外的数据,会带来更好的用户体验.Ajax技术核心:XMLHttpRequest对象(简称XHR).XHR为向服务器发送请求和解析服务器响应提供了流畅的接口.1.创建XM ...