Gradle 构建工具 #5 又冲突了!如何理解依赖冲突与版本决议?

3,062 阅读15分钟

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。

Gradle 作为官方主推的构建系统,目前已经深度应用于 Android 的多个技术体系中,例如组件化开发、产物构建、单元测试等。可见,要成为 Android 高级工程师 Gradle 是必须掌握的知识点。

本文是 Gradle 构建工具系列的第 5 篇文章,完整文章目录请移步到文章末尾~

前言

大家好,我是小彭。

在前文 Gradle 构建工具 #3 Maven 发布插件使用攻略(以 Nexus / Jitpack 为例)Gradle 构建工具 #4 来开源吧!发布开源组件到 MavenCentral 仓库超详细攻略 文章中,我们已经讨论过如何发布组件到 Nexus 企业私有仓库或 MavenCentral 中央仓库的方法。

在发布组件的新版本时,开发者需要描述该组件的 GAV 基本信息,包括:groupId、artifactId、version 和 packaging 等。在协同开发的另一侧,依赖方也需要通过相同的 GAV 坐标来定位依赖项:

build.gradle

dependencies {
    implementation 'io.github.pengxurui:modular-eventbus-annotation:1.0.0'
}

然而,当工程中的依赖关系增多就很容易会遇到依赖版本冲突问题,这个时候 Gradle 构建工具是否有统一的规则来处理冲突,而开发者又需要采用什么样的手段来应对冲突呢?


目录

1、如何声明依赖版本?

1.1 静态版本与不稳定版本的区别(What & What's Diff)

1.2 动态版本和变化版本的区别(What & What's Diff)

1.3 如何调整不稳定版本的解析策略(How)

2、依赖冲突是怎么发生的?

2.1 什么是依赖传递(What)

2.2 什么是依赖冲突(What)

2.3 如何查看依赖版本冲突(How)

3、Gradle 依赖版本决议

3.1 对比 Maven 和 Gradle 的解析策略(What’s Diff)

3.2 版本排序规则(Detail)

3.3 Dependency API:strictly、require、reject、prefer、exclude、transitive(Detail)

3.4 DependencyConstraintHandler API(Detail)

3.5 ResolutionStrategy API(Detail)

4、总结


1. 如何声明依赖版本?

首先,我们先盘点出 Gradle 构建系统中声明依赖版本的方式:

1.1 静态版本与不稳定版本

在 Gradle 构建声明依赖的语法想必各位都了然于胸了:

build.gradle

dependencies {
    // 简写格式
    implementation 'com.google.guava:guava.20.0'
    // 完整格式:
    implementation group 'com.google.guava', name: 'guava:guava', version '20.0'
}

其实 Gradle 不仅支持精确地指定版本号外,还支持丰富的版本声明方法,我这里总结了一些比较实用的使用方式:

  • 静态版本(精确版本): 最简单的方式,例如 1.1

  • 区间版本: 使用 () 或 [] 定义开闭区间,例如 [1.0,) 表示高于 1.0 版本

  • 前缀版本: 通过 + 指定版本号前缀,相当于特殊的区间版本,例如 1.1.+

  • 最新版本: 通过 latest-status 指定最新版本,例如 latest-release

  • SNAPSHOT 版本: Maven 风格的快照版本,例如 1.1-SNAPSHOT

除了精确版本外,其它所有的版本声明方式的构建都是不稳定的,比如 [1.0,) 到底是依赖 1.1 还是 1.2?而 1.1.+ 到底是依赖 1.1.0 还是 1.1.1?

那么,这些不稳定版本存在的意义是什么?

1.2 如何理解两种不稳定版本 —— 动态版本和变化版本

我原本是计划将静态版本以外的声明方式理解为「动态版本」,但是按照 Gradle 官方文档来理解的话,其实会细分为「Dynamic Version 动态版本」和「Changing Version 变化版本」,为避免混淆概念,我们就统一将后者理解为「不稳定版本」好了。

可是,Gradle 官方的也未免太学术化了吧 🤕 应该如何理解呢?

一句话概括:

「动态版本是版本不稳定,变化版本是产物不稳定」

  • Dynamic 动态版本

动态版本是指版本号不固定的声明方式,例如前面提到的区间版本、前缀版本和最新版本都属于动态化版本,最终依赖的版本号之后在构建时才能确定(如 2.+⇒2.3 只有在构建时才能确定)。

因此,动态版本适合用在强调使用依赖项最新版本的场景,项目会更加积极地拥抱依赖项的最新版本,当仓库中存在依赖项的最新版本时,动态版本直接解析为依赖项的最新版本(还需要满足缓存超时的前提)。

  • Changing 变化版本

变化版本是指版本号固定但产物不固定的声明方式,比如 Maven 的 SNAPSHOT 快照版本。快照版本会在每次构建时到远程仓库中检查依赖项产物的最新版本(还需要满足缓存超时的前提)。

例如,在大型软件项目中,往往是多个团队(或多名同学)协同开发不同模块,例如 A 模块依赖 B 模块,两个模块并行开发。如果模块 B 不使用快照版本(例如版本为 1.0.0),那么当 B 模块在开发阶段需要更新,A 模块就无法接收到更新。因为 A 模块本地仓库中已经下载了 B 模块的 1.0.0 版本,所以构建时不会重复去下载远程仓库中更新的版本。

直接的解决办法可以清除 A 模块的本地仓库缓存,或者每次 B 模块更新都升级版本,很显然两个办法都不灵活,频繁升级版本也是对版本号的滥用,不利于版本管理。而如果模块 B 使用快照版本(1.0.0-SNAPSHOT),A 模块每次构建都会去检查远程仓库是否有 B 模块的新快照(还需要满足缓存超时的前提),就可以保证一直依赖 B 模块的最新版本。

总的来说,动态版本倾向于积极拥抱最新版本,而快照版本倾向于积极集成开发版本,要根据具体的协同开发场景来选择,在实践经验中,变化版本(快照版本)的使用频率更大。

需要注意的是:这两种版本均不应该用在生产环境配置中,因为这两种不稳定版本共同存在的问题是: 「输入相同的构建配置可能会产生不同的构建产物输出」 ,会导致重复构建正式产物的不确定性。在实践中,也确实暴露过一些不稳定版本滥用而造成的生产事故,最终我和同事优化了这个问题,这个我们后文再分享(没错,我又来挖坑了)。

1.3 调整不稳定版本的解析策略

在默认情况下, Gradle 会按照 24 小时缓存有效期缓存动态版本和变化版本的解析结果,在缓存有效期间,Gradle 不会检查远程仓库来获取最新的依赖项。在默认配置的基础上,Gradle 还提供了「时间和锁定」两个层面来控制不稳定版本的解析策略的 API:

By default, Gradle caches changing versions of dependencies for 24 hours, … By default, Gradle caches dynamic versions and changing modules for 24 hours, …

  • 修改缓存时间

通过修改依赖分组的 ResolutionStrategy 决议策略对象,可以修改缓存时间:

build.gradle

configurations.all {
    // 修改 Dynamic 版本的缓存时间
    resolutionStrategy.cacheDynamicVersionsFor 10, 'minutes'
    // 修改 Changing 版本的缓存时间
    resolutionStrategy.cacheChangingModulesFor 10, 'minutes'
}
  • 锁定动态版本

通过控制依赖分组的 ResolutionStrategy 决议策略对象,可以设置版本锁定,但只针对动态版本有效,对于变化版本(快照版本)不生效。版本锁定的细节比较多,目前在社区上没查找到开发者的应用实践,我们就先不展开了(又挖坑?)

build.gradle

configurations {
    compileClasspath {
        resolutionStrategy.activateDependencyLocking()
    }
}

🙋🏻‍♀️ 现在有一个疑问:既然 Gradle 都会按照解析规则选择精确精确版本或者不稳定版本的最新版本。那么,我们说的依赖冲突到底是怎么发生的呢?


2. 依赖冲突是怎么发生的?

2.1 什么是依赖传递?

用最简单的话说,A 依赖 B,B 依赖 C,那么 A 也会依赖 C,这就是依赖传递。

在 Gradle 生命周期的配置阶段,Gradle 会解析组件之间的依赖关系。当一个组件被添加到依赖关系图中时,还会递归地解析该组件所依赖的其他组件,同时将「间接依赖」也添加到依赖关系图中,直到组件自身没有依赖时终止。

  • Direct Dependency 直接依赖

表示模块需要直接依赖和使用的特性,例如模块依赖了 com.squareup.okhttp3:okhttp,那么 OkHttp 就是直接依赖;

  • Transitive Dependency 间接依赖

如果在被直接依赖的组件中,如果该组件还依赖了其他组件,那么其它组件就被间接依赖,例如 com.squareup.okio:okio Okio 就是间接依赖。

这就是 Gradle 的依赖传递,很容易理解吧。

2.2 什么是依赖依赖冲突?

在大型项目中,当工程中的依赖关系增多就很容易会遇到依赖冲突问题,想必各位在工作中也遇到过各种各样的依赖冲突问题。你遇到过什么样的依赖冲突问题,可以在评论区发表一下观点 🔽 

社区中通常会将依赖冲突和依赖版本冲突划上等号,比如 20 年百度 App 技术团队的公开资料 《Gradle 与 Android 构建入门》。其实,如果我们结合实践中暴露的问题,Gradle 的依赖冲突可以细分为 2 类问题:

  • Version Conflict 版本冲突: 在项目依赖关系图中,某个依赖项存在多个版本;

  • Implementation conflict 实现冲突: 在项目依赖关系图中,多个依赖项存在相同实现。

版本冲突大家都很熟悉,我们今天要讨论就是版本决议问题。

那么「实现冲突」又怎么理解呢,两个组件存在相同实现听起来就很离谱啊 🌚 

其实把 Build Output 报错日志贴出来,你就懂了。

Build Output

> Task :app:checkDebugDuplicateClasses FAILED
Duplicate class org.objectweb.asm.AnnotationVisitor found in modules asm-3.3.1 (asm:asm:3.3.1) and asm-4.0 (org.ow2.asm:asm:4.0)
Duplicate class org.objectweb.asm.AnnotationWriter found in modules asm-3.3.1 (asm:asm:3.3.1) and asm-4.0 (org.ow2.asm:asm:4.0)
...

由于项目依赖中 "asm:asm:3.3.1" 和 "org.ow2.asm:asm:4.0" 都存在相同的 ASM 特性,所以当依赖关系树中存在两个相同实现时,构建就 Fail 掉了,不可能同一个类打包两份对吧。

build.gradle

dependencies {
    implementation "asm:asm:3.3.1"
    implementation "org.ow2.asm:asm:4.0"
}

源码

// asm:asm:3.3.1
package org.objectweb.asm;

public interface AnnotationVisitor {

}

// org.ow2.asm:asm:4.0
package org.objectweb.asm;

public abstract class AnnotationVisitor {
}

老司机们见多识广,懂的都懂 🌚

2.3 如何查看依赖版本冲突?

相比于依赖实现冲突,依赖版本冲突通常更加隐蔽,毕竟不同版本之间会考虑兼容性,所以构建时不会直接构建失败(构建成功不代表运行时不会 Crash,这是一个坑哦 😁)

那么,我们怎么查看工程中存在的依赖版本冲突呢,方法比较多:

  • 1、Task dependencies

  • 2、Task dependencyInsight

  • 3、Build Scan

  • 4、新版 Android Studio 的 Gradle Dependency Analyzer 分析器(推荐)

// 依赖树信息:
androidx.savedstate:savedstate-ktx:1.2.0

androidx.annotation:annotation:1.0.0 -> 1.5.0 (*)

org.jetbrains.kotlin:kotlin-stdlib:1.7.10 (*)

androidx.collection:collection:{strictly 1.0.0} -> 1.0.0 (c)
  • >:表示冲突,比如这个1.1.0 -> 1.3.0,> 表示 1.1.0 版本被拉高到 1.3.0;

  • 表示省略不重要的层级;

  • c:c 是 constraints 的简称,表示 DependencyConstraintHandler API 约束的版本;

  • strictly:表示 Dependency API strictly 强制指定的版本。

理解了依赖传递和依赖冲突后,现在我们来讨论 Gradle 的依赖版本决议机制:


3. Gradle 依赖版本决议

比如以下依赖关系中,项目工程中直接或间接依赖 OkHttp 的两个版本,可以看到依赖关系树上存在 okhttp:3.10.0 和 okhttp 3.14.9 两个版本:

  • 直接依赖 com.squareup.okhttp3:okhttp:3.10.0

  • 直接依赖 com.squareup.retrofit2:retrofit:2.9.0 → com.squareup.okhttp3:okhttp:3.14.9

dependencies {
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation "com.squareup.okhttp3:okhttp:3.10.0"
}

现在的问题是:Gradle 应该选择哪个依赖项版本呢?

这就是版本决议(Dependency Resolution)要讨论的问题,结论先行 👉🏻

Gralde 依赖版本决议会综合考虑依赖关系图上所有的直接依赖、间接依赖和依赖约束规则(API),并从中选择出符合所有约束规则的最高依赖项版本。如果不存在满足约束规则的依赖项版本,则会抛出构建失败错误。

When Gradle attempts to resolve a dependency to a module version, all dependency declarations with version, all transitive dependencies and all dependency constraints for that module are taken into consideration. The highest version that matches all conditions is selected. If no such version is found, Gradle fails with an error showing the conflicting declarations. —— 官方文档原文

我把这个结论可视化出来,就很清晰了:

我们把依赖信息打印出来,也确实是采用最高版本:

+--- com.squareup.retrofit2:retrofit:2.9.0
|    \--- com.squareup.okhttp3:okhttp:3.14.9
|         \--- com.squareup.okio:okio:1.17.2
\--- com.squareup.okhttp3:okhttp:3.10.0 -> 3.14.9 (*)

3.1 对比 Maven 和 Gradle 的解析策略

不同的构建系统设计的解析策略不同,我们以 Maven 为对比:

  • Maven 最短路径策略

Maven 构建系统会采用最短路策略,构建系统会选择从根模块到依赖项的最短路来选择版本。例如在本节开头的例子总,在 Maven 构建系统中就会选择 com.squareup.okhttp3:okhttp:3.10.0 这个版本。

  • Gradle 最高版本策略

Gradle 构建系统会采用最高版本策略,构建系统会选择依赖关系图中满足约束规则的最高版本。例如在本节开头的例子中,在 Gradle 构建系统中就会选择 com.squareup.okhttp3:okhttp:3.14.9 这个版本。

一个误区: 需要避免混淆的是,在 Gradle 中使用 Maven 仓库,并不会左右 Gradle 的冲突解决策略,这里的 Maven 仓库仅用于提供依赖项,而依赖管理依然是在 Gradle 的框架内运行的。

3.2 版本排序规则(面试题)

OK,既然在出现版本冲突时,Gradle 会选择依赖关系图中最高的版本号,那么版本号的排序规则是怎样的呢?比如 1.1.0-alpha 和 1.0.0 会选择哪个版本呢?完整的规则文档在 Declaring Versions and Ranges 中。

有毒啊,文档这也太复杂了哦,我将整个文档提炼为 3 条基本规则,已经可以满足大部分开发场景了:

  • 1、分段对比规则 版本号字符串会被分隔符划分为多个分段,高分段优先:

    • 1.1 分隔符: 支持使用 [.-_+] 分隔符,分隔符没有差异,即 1.a.1 == 1-a-1
    • 1.2 字母和数字分开: 字母和数字会划分为不同分段,即 1a1 存在三个级别,和 1a1 == 1.a.1
    • 1.3 高级别优先: 高级别分段优先确定版本高低,即 2.1 > 1.2
  • 2、同分段对比规则 同分段中,数字按数值排序,数字优先于字母:

    • 2.1 数字版本高于字母版本: 即 1.1 > 1.a
    • 2.2 数字版本按数值排序: 即 1.10 > 1.2(易错,并不是按照「字典排序」规则,如果按照字典排序 1.2 > 1.10)
    • 2.3 字母版本按字母顺序排序,大写优先: 即 1.Bc > 1.B > 1.A > 1.a
  • 3、特殊字符串规则 特殊字符串有特殊的排序规则:

    • 3.1 发布序列: 即 1.0-dev < 1.0-alpha- < 1.0-rc < 1.0-release < 1.0
    • 3.2 snapshot 快照版本低于正式版本: 即 1.0-rc < 1.0-snapshot < 1.0-release < 1.0

就是说 Gradle 会分段对齐对比,字母和数字属于不同分段,而同级别分段按照数值排序,而不是字典序排序。OK,那我明白了,按规则排列 1.1.0-alpha < 1.0.0 的,因此会选择 1.0.0(Gradle 最高版本策略)这个版本。

虽然 Gradle 在平台层提供了一套依赖解析决议机制,但 Gradle 版本决议的默认规则是选择的最高版本,最高版本不一定与项目兼容,所以开发者有时候要使用版本决议规则 API 来配置和干预 Gradle 的决议规则。

3.3 Dependency API

  • strictly 严格版本: 强制选择此版本,由于 Gradle 采用高版本优先策略,因此 strictly 的应用场景是为了降低版本(等价于 !! 双感叹号语法):
dependencies {
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation("com.squareup.okhttp3:okhttp") {
        version{
            strictly("3.10.0")
        }
    }
    // 等价于
    implementation("com.squareup.okhttp3:okhttp:3.10.0!!") 
}
+--- com.squareup.retrofit2:retrofit:2.9.0
|    \--- com.squareup.okhttp3:okhttp:3.14.9 -> 3.10.0
|         \--- com.squareup.okio:okio:1.14.0
\--- com.squareup.okhttp3:okhttp:{strictly 3.10.0} -> 3.10.0 (*)
  • require 最低版本: 不低于此版本
dependencies {
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation("com.squareup.okhttp3:okhttp") {
        version{
            require("3.10.0")
        }
    }
}
+--- com.squareup.retrofit2:retrofit:2.9.0
|    \--- com.squareup.okhttp3:okhttp:3.14.9
|         \--- com.squareup.okio:okio:1.17.2
\--- com.squareup.okhttp3:okhttp:3.10.0 -> 3.14.9 (*)
  • reject 拒绝版本: 拒绝选择此版本
dependencies {
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation("com.squareup.okhttp3:okhttp") {
        version{
            reject("3.10.0")
        }
    }
}
+--- com.squareup.retrofit2:retrofit:2.9.0
|    \--- com.squareup.okhttp3:okhttp:3.14.9
|         \--- com.squareup.okio:okio:1.17.2
\--- com.squareup.okhttp3:okhttp:{reject 3.10.0} -> 3.14.9 (*)
  • prefer 优先版本: 如果不存在更高版本,则优先使用此版本。
dependencies {
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation("com.squareup.okhttp3:okhttp") {
        version{
            prefer("3.10.0")
        }
    }
}
+--- com.squareup.retrofit2:retrofit:2.9.0
|    \--- com.squareup.okhttp3:okhttp:3.14.9
|         \--- com.squareup.okio:okio:1.17.2
\--- com.squareup.okhttp3:okhttp:{prefer 3.10.0} -> 3.14.9 (*)

需要注意的时,strictly 和 require 语句会相互覆盖,要以最后声明的语句为准,strictly 和 require 语句还会清除之前声明的 reject 语句,因此应该把 reject 语句放在最后。

dependencies {
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation("com.squareup.okhttp3:okhttp") {
        version{
            require '3.10.0'
            reject '3.14.9'
            strictly '4.10.0'
            prefer '3.10.0'
        }
    }
}
+--- com.squareup.retrofit2:retrofit:2.9.0
|    \--- com.squareup.okhttp3:okhttp:3.14.9 -> 4.10.0
|         +--- com.squareup.okio:okio:3.0.0
|         |    \--- com.squareup.okio:okio-jvm:3.0.0
|         |         +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31 -> 1.7.20 (*)
|         |         \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31 -> 1.7.20
|         \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.20 -> 1.7.20 (*)
\--- com.squareup.okhttp3:okhttp:{strictly 4.10.0; prefer 3.10.0} -> 4.10.0 (*)
  • exclude 排除规则

使用 exclude 可以根据 GAV 坐标排除间接依赖,也常用于解决前面提到的依赖实现冲突问题。

dependencies {
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation("com.squareup.okhttp3:okhttp") {
        version{
            require '3.10.0'
            reject '3.14.9'
            strictly '4.10.0'
            prefer '3.10.0'
        }
    }
}
+--- com.squareup.retrofit2:retrofit:2.9.0
\--- com.squareup.okhttp3:okhttp:3.10.0
     \--- com.squareup.okio:okio:1.14.0
  • transitive 传递规则

使用 transitive 可以控制是否传递间接依赖:

dependencies {
    implementation("com.squareup.retrofit2:retrofit:2.9.0") {
        transitive(false) // 不传递
    }
    implementation("com.squareup.okhttp3:okhttp:3.10.0")
}
+--- com.squareup.retrofit2:retrofit:2.9.0
\--- com.squareup.okhttp3:okhttp:3.10.0
     \--- com.squareup.okio:okio:1.14.0

3.4 DependencyConstraintHandler API

constraints 约束规则提供了一个统一的位置来控制项目的依赖版本,而在声明依赖的位置甚至可以不需要指定版本。但是如果模块想单独编译,那么还是需要指定版本的,毕竟没有约束源就无法确定版本。

子模块 build.gradle

dependencies {
    implementation("com.squareup.retrofit2:retrofit") // 不指定版本
    implementation("com.squareup.okhttp3:okhttp:3.10.0") // 指定 3.10.0
}

主模块 build.gradle

dependencies {
    implementation project(':mylibrary')
}

dependencies {
    constraints {
        implementation 'com.squareup.retrofit2:retrofit:2.9.0' // 指定版本
        implementation('com.squareup.okhttp3:okhttp') {
            version {
                strictly("4.10.0") // 强制修改版本
            }
        }
    }
}

打印子模块的依赖信息:

+--- com.squareup.retrofit2:retrofit FAILED // 无法解析(单独编译缺少约束来源) 
\--- com.squareup.okhttp3:okhttp:3.10.0
     \--- com.squareup.okio:okio:1.14.0

打印主模块的依赖信息:

+--- project :mylibrary
|    +--- com.squareup.retrofit2:retrofit -> 2.9.0
|    |    \--- com.squareup.okhttp3:okhttp:3.14.9 -> 4.10.0 // 强制修改版本
|    |         +--- com.squareup.okio:okio:3.0.0
|    |         |    \--- com.squareup.okio:okio-jvm:3.0.0
|    |         |         +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31 -> 1.7.20 (*)
|    |         |         \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31 -> 1.7.20
|    |         \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.20 -> 1.7.20 (*)
|    \--- com.squareup.okhttp3:okhttp:3.10.0 -> 4.10.0 (*)
+--- com.squareup.retrofit2:retrofit:2.9.0 (c)
\--- com.squareup.okhttp3:okhttp:{strictly 4.10.0} -> 4.10.0 (c)

3.5 ResolutionStrategy API

Configuration 提供一个 ResolutionStrategy 策略,ResolutionStrategy API 的优先级是比 Dependency API 和 DependencyConstraintHandler API 更高的,可以最为后置手段统一更改依赖库版本。

主模块 build.gradle

dependencies {
    implementation project(':mylibrary')
}

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.squareup.okhttp3' && requested.name == 'okhttp') {
            details.useVersion '4.10.0' // 强制修改版本
        }
    }
}

dependencies {
    constraints {
        // implementation 'com.squareup.retrofit2:retrofit:2.9.0' // 不指定版本,ResolutionStrategy API 也能解析
        implementation('com.squareup.okhttp3:okhttp') {
            version {
                strictly("3.10.0") // 强制修改版本
            }
        }
    }
}

打印子模块的依赖信息:

+--- project :mylibrary
|    +--- com.squareup.retrofit2:retrofit -> 2.9.0
|    |    \--- com.squareup.okhttp3:okhttp:3.14.9 -> 4.10.0
|    |         +--- com.squareup.okio:okio:3.0.0
|    |         |    \--- com.squareup.okio:okio-jvm:3.0.0
|    |         |         +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31 -> 1.7.20 (*)
|    |         |         \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31 -> 1.7.20
|    |         \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.20 -> 1.7.20 (*)
|    \--- com.squareup.okhttp3:okhttp:3.10.0 -> 4.10.0 (*)
+--- com.squareup.retrofit2:retrofit:2.9.0 (c)
\--- com.squareup.okhttp3:okhttp:{strictly 3.10.0} -> 4.10.0 (c)

4. 总结

  • 1、在 Gradle 构建工具中可以声明稳定版本和不稳定版本,其中不稳定版本中的 Dynamic 变化版本指版本号不稳定,而 Changing 变化版本(如 SNAPSHOT)指产物不稳定;

  • 2、Gralde 依赖版本决议机制会综合考虑依赖关系图上所有的直接依赖、间接依赖和依赖约束规则(API),并从中选择出符合所有约束规则的最高依赖项版本。如果不存在满足约束规则的依赖项版本,则会抛出构建失败错误;

  • 3、虽然 Gradle 在平台层提供了一套依赖解析决议机制,但 Gradle 版本决议的默认规则是选择的最高版本,最高版本不一定与项目兼容,所以需要开发者使用相关版本决议规则 API 来配置和干预 Gradle 的决议规则。

今天我们学习了 Gradle 的依赖冲突与版本决议原理,在下一篇文章中我们将会落实到 Gradle 源码上进行分析,请关注。


参考资料

推荐阅读

Gradle 构建工具完整目录如下(2023/07/12 更新):

整理中...

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~