本章解释 Gradle 中依赖项解析的工作原理。在学习如何声明 依赖项指定项目应使用的版本 后,下一步是了解如何在依赖项解析过程中组合这些声明。

依赖项解析发生在两个关键阶段,重复进行直到构建完整的依赖关系图

  1. 冲突解决:

    • 当引入新的依赖项时,Gradle 会解析任何冲突,以确定应添加到图中的版本。

    • Gradle 将应用其冲突解决规则(例如,“最新版本胜出”或自定义版本解析策略)来确定要使用的版本。

  2. 依赖项元数据检索:

    • 一旦特定依赖项(具有版本的模块)包含在图中,Gradle 将检索其元数据(例如 POM、Ivy 或模块元数据)以

      • 将其自身的依赖项(传递依赖项)添加到图中。

      • 了解该依赖项的可用变体。

此过程持续进行,直到解析完整棵依赖树。

dependency resolution model

阶段 1. 冲突解决

执行依赖项解析时,Gradle 处理两种类型的冲突

  1. 版本冲突:当多个依赖项请求相同的依赖项但版本不同时发生。Gradle 必须选择要包含在图中的版本。

  2. 实现/能力冲突:当依赖关系图包含提供相同功能或能力的不同模块时发生。Gradle 通过选择一个模块来避免重复实现来解决这些冲突。

依赖项解析过程是高度可定制的,许多 API 可以影响该过程。

A. 版本冲突

当两个组件

  • 依赖于同一模块,例如 com.google.guava:guava

  • 但版本不同,例如 20.025.1-android

    • 我们的项目直接依赖于 com.google.guava:guava:20.0

    • 我们的项目也依赖于 com.google.inject:guice:4.2.2,而后者又依赖于 com.google.guava:guava:25.1-android

Gradle 必须通过选择一个版本来包含在依赖关系图中来解决此冲突。

Gradle 会考虑整个依赖关系图中所有请求的版本,默认情况下,选择最高版本。详细的版本排序在 版本排序 中进行了解释。

Gradle 还支持 丰富的版本声明 的概念,这意味着“最高”版本的构成取决于版本的声明方式

  • 没有范围:将选择最高的非拒绝版本。

    • 如果声明的 strictly 版本低于最高版本,则解析将失败。

  • 有范围:

    • 如果非范围版本在范围内或高于上限,则将选择该版本。

    • 如果仅存在范围,则选择取决于这些范围的交集

      • 如果范围重叠,则选择交集中最高的现有版本。

      • 如果不存在明确的交集,则将选择最大范围内的最高版本。如果最高范围内不存在任何版本,则解析失败。

    • 如果声明的 strictly 版本低于最高版本,则解析将失败。

对于版本范围,Gradle 需要执行中间元数据查找,以确定可用的变体,如 阶段 2. 依赖项元数据检索 中所述。

带有限定符的版本

术语“限定符”是指版本字符串中跟在非点分隔符(如连字符或下划线)之后的部分。

例如

原始版本 基本版本 限定符

1.2.3

1.2.3

<无>

1.2-3

1.2

3

1_alpha

1

alpha

abc

abc

<无>

1.2b3

1.2

b3

abc.1+3

abc.1

3

b1-2-3.3

b

1-2-3.3

如您所见,分隔符是 .-_+ 字符中的任何一个,以及当版本的数字部分和非数字部分彼此相邻时的空字符串。

默认情况下,Gradle 在解决冲突时优先选择没有限定符的版本。

例如,在版本 1.0-beta 中,基本形式是 1.0,而 beta 是限定符。没有限定符的版本被认为更稳定,因此 Gradle 将优先考虑它们。

以下是一些示例以进行澄清

  • 1.0.0(无限定符)

  • 1.0.0-beta(限定符:beta

  • 2.1-rc1(限定符:rc1

即使限定符在词典编纂上更高,Gradle 通常也会认为像 1.0.0 这样的版本高于 1.0.0-beta

在解决版本之间的冲突时,Gradle 应用以下逻辑

  1. 基本版本比较: Gradle 首先选择具有最高基本版本的版本,忽略任何限定符。所有其他版本都将被丢弃。

  2. 限定符处理: 如果仍然有多个版本具有相同的基本版本,Gradle 将选择一个,优先选择没有限定符的版本(即,发行版本)。如果所有版本都具有限定符,Gradle 将考虑限定符的顺序,优先选择更稳定的限定符,例如 “release”,而不是其他限定符,例如 “beta” 或 “alpha”。

B. 实现/能力冲突

Gradle 使用变体能力来定义模块提供的内容。

变体 本质上是依赖项的不同形式,通常基于平台(例如,JVM 或 Android)或配置(例如,编译、运行时)等因素。

能力 是一种表达依赖项的互斥变体的方式。

冲突 在以下情况下发生

  • 不兼容的变体:当两个模块尝试选择依赖项的不同、不兼容的变体时。

  • 相同能力:当多个模块声明相同的能力时,会在功能上造成重叠。

有关变体选择如何工作以及它如何实现灵活的依赖项管理的更多详细信息,请参阅下面的 variant_model.html

阶段 2. 依赖项元数据检索

Gradle 在依赖关系图中需要模块元数据的原因有两个

  1. 确定动态依赖项的现有版本:当指定动态版本(如 1.+latest.release)时,Gradle 必须识别可用的具体版本。

  2. 解析特定版本的模块依赖项:Gradle 检索与模块关联的依赖项(基于指定的版本),确保将正确的传递依赖项包含在构建中。

A. 确定动态依赖项的现有版本

当面对动态版本时,Gradle 必须通过以下步骤识别可用的具体版本

  1. 检查仓库:Gradle 按照添加顺序检查每个定义的仓库。它不会在第一个返回元数据的仓库处停止,而是会继续检查所有可用的仓库。

  2. Maven 仓库:Gradle 从 maven-metadata.xml 文件中检索版本信息,该文件列出了可用的版本。

  3. Ivy 仓库:Gradle 诉诸目录列表来收集可用版本。

结果是 Gradle 评估并与动态版本匹配的候选版本列表。Gradle 缓存 此信息以优化未来的解析。此时,版本冲突解决 恢复。

B. 解析特定版本的模块依赖项

当 Gradle 尝试解析具有特定版本的所需依赖项时,它遵循以下流程

  1. 仓库检查:Gradle 按照定义顺序检查每个仓库。

    • 它查找描述模块的元数据文件(.module.pomivy.xml),或直接查找工件文件。

    • 具有元数据文件(.module.pomivy.xml)的模块优先于仅具有工件文件的模块。

    • 一旦在仓库中找到元数据,后续仓库将被忽略。

  2. 检索和解析元数据:如果找到元数据,则会对其进行解析。

    • 如果 POM 文件具有父 POM,则 Gradle 会递归解析每个父模块。

  3. 请求工件:模块的所有工件都从提供元数据的同一仓库中获取。

  4. 缓存:所有数据,包括仓库源和任何潜在的遗漏,都存储在 依赖项缓存 中以供将来使用。

上面的重点突出了集成 Maven Local 的潜在问题。由于 Maven Local 充当 Maven 缓存,因此它可能偶尔会遗漏模块的工件。当 Gradle 从 Maven Local 获取模块并且工件丢失时,它会假定这些工件完全不可用。

禁用仓库

当 Gradle 无法从仓库检索信息时,它会禁用该仓库以用于构建的剩余部分,并使所有依赖项解析失败。

此行为确保可重现性。

如果在忽略故障仓库的情况下继续构建,则一旦仓库恢复在线,后续构建可能会产生不同的结果。

HTTP 重试

Gradle 将尝试多次连接到仓库,然后再禁用它。如果连接失败,Gradle 会在特定的临时错误上重试,重试之间等待时间会逐渐增加。

当仓库无法访问时(由于永久性错误或在最大重试次数已用尽之后),该仓库将被标记为不可用。

依赖树

一旦过程完成,就会创建依赖树。

依赖树是项目所需的所有依赖项的分层表示,包括直接依赖项(显式声明的)和传递依赖项(由这些直接依赖项自动引入)。该图显示了依赖项如何相互关联以及 Gradle 如何解析它们。

对于依赖项 org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1,该图包括主要依赖项及其所有传递依赖项。以下是该图的样子

org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1
├── JVM variant
│   ├── org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1
│   │   ├── org.jetbrains.kotlin:kotlin-stdlib:1.8.0
│   │   ├── org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0
│   │   └── org.jetbrains:annotations:13.0
│   ├── org.jetbrains.kotlin:kotlin-stdlib:1.8.0
│   ├── org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0
│   └── org.jetbrains:annotations:13.0
│
├── Android variant
│   ├── org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1
│   │   ├── org.jetbrains.kotlin:kotlin-stdlib:1.8.0
│   │   ├── org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0
│   │   └── org.jetbrains:annotations:13.0
│   ├── org.jetbrains.kotlin:kotlin-stdlib:1.8.0
│   ├── org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0
│   ├── org.jetbrains:annotations:13.0
│   └── com.android.tools:common-library:1.0.0
│
├── Native variant
│   ├── org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1
│   │   ├── org.jetbrains.kotlin:kotlin-stdlib:1.8.0
│   │   ├── org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0
│   │   └── org.jetbrains:annotations:13.0
│   ├── org.jetbrains.kotlin:kotlin-stdlib:1.8.0
│   ├── org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0
│   ├── org.jetbrains:annotations:13.0
│   └── kotlinx.coroutines:kotlinx-coroutines-core-native:1.6.4
│
└── JavaScript variant
    ├── org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1
    │   ├── org.jetbrains.kotlin:kotlin-stdlib-js:1.8.0
    │   └── org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0
    ├── org.jetbrains.kotlin:kotlin-stdlib-js:1.8.0
    ├── org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0
    └── kotlinx.coroutines:kotlinx-coroutines-core-js:1.6.4