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

news2024/12/24 20:53:43

⭐️ 本文已收录到 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 源码上进行分析,请关注。


参考资料

  • Working with Dependencies —— Gradle 官方文档
  • Gradle 与 Android 构建入门 —— xuduokai(百度)著
  • 一文搞懂 Gradle 的依赖管理和版本决议 —— yechaoa(阿里)著

推荐阅读

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

  • #1 为什么说 Gradle 是 Android 进阶绕不去的坎

  • #2 手把手带你自定义 Gradle 插件

  • #3 Maven 发布插件使用攻略(以 Nexus / Jitpack 为例)

  • #4 来开源吧!发布开源组件到 MavenCentral 仓库超详细攻略

  • #5 又冲突了!如何理解依赖冲突与版本决议?

整理中…

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/761188.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STM32(HAL库)驱动SHT30温湿度传感器通过串口进行打印

目录 1、简介 2、CubeMX初始化配置 2.1 基础配置 2.1.1 SYS配置 2.1.2 RCC配置 2.2 软件IIC引脚配置 2.3 串口外设配置 2.4 项目生成 3、KEIL端程序整合 3.1 串口重映射 3.2 SHT30驱动添加 3.3 主函数代 3.4 效果展示 1、简介 本文通过STM32F103C8T6单片机通过HAL库…

Spring Batch之读数据库——JdbcCursorItemReader之自定义RowMapper(三十七)

一、自定义RowMapper 详情参考我的另一篇博客&#xff1a; Spring Batch之读数据库——JdbcCursorItemReader&#xff08;三十五&#xff09;_人……杰的博客-CSDN博客 二、项目实例 1.项目框架 2.代码实现 BatchMain.java: package com.xj.demo28;import org.springfram…

代码随想录第27天 | 455.分发饼干 ● 376. 摆动序列 ● 53. 最大子序和

455.分发饼干 /*** param {number[]} g* param {number[]} s* return {number}*/ var findContentChildren function(g, s) {let a0let b0let count0g.sort((a,b)>a-b)s.sort((a,b)>a-b)while(a<g.length&&b<s.length){if(s[b]>g[a]){countba}else{b}…

STM32(HAL库)软件IIC驱动OLED

目录 1、简介 2、CubeMX初始化配置 2.1 基础配置 2.1.1 SYS配置 2.1.2 RCC配置 2.2 软件IIC引脚配置 2.3 项目生成 3、KEIL端程序整合 3.1 OLED驱动添加 3.3 主函数代 3.4 效果展示 1、简介 本文通过STM32F103C8T6单片机&#xff08;HAL库&#xff09;通过软件IIC方式…

java linux服务器环境搭建

安装 jdk 下载jdk: wget --no-cookies --no-check-certificate --header "Cookie: gpw_e24http%3A%2F%2Fwww.oracle.com%2F; oraclelicenseaccept-securebackup-cookie" "http://download.oracle.com/otn-pub/java/jdk/8u141-b15/336fa29ff2bb4ef291e347e091f7f…

Kubespray v2.22.1 在线部署 kubernetes v1.26.5 集群

文章目录 1. 介绍2. 预备条件3. 配置 hostname4. yum5. 下载介质5.1 git 下载5.2 下载 kubespray v2.22.1 6. 编写 inventory.ini7. 配置互信8. 安装 ansible9. 关闭防火墙10. 安装 docker11. 配置内核参数12. 启动容器 kubespray13. 部署14. 配置连接集群 1. 介绍 kubespray​…

Ubuntu18.04 安装vscode 配置C#编译器

环境&#xff1a; ubuntu 18.04 依赖库&#xff1a; SDK .net-7 安装对象&#xff1a; vscode 在终端&#xff1a; ./dotnet-install.sh --channel 7.0 遇见如下提示&#xff1a; dotnet&#xff1a;未找到命令 如下操作&#xff1a; 下载–解压–安装 wget https://pa…

Python 自学 day04 函数为参数传递, 匿名函数, 文件操作

1. 函数作为参数传递 &#xff08;类似C 函数指针&#xff09; def func(x):mm x(1,2);#print(f"mm的值是{mm}")return mmdef add(x,y): #加法return xy def reduce(x,y): # 减法return x-ydef ride(x,y): # 乘法return x*ydef divide(x,y): #带有小数点除法…

详解DDPG算法:解决对大量的超参数、随机重启、任务环境敏感问题,完成月球着陆器,双足机器人demo、以及超参数调优教学

0.demo展示 当我复现强化学习算法 DDPG 时,我发现论文中缺少必要的实现细节,例如:Gamma、噪声方差、最大训练步数等参数的取值。此外,在我调整参数,成功完成某次训练后,当我对随机种子进行修改,发现训练时长有很大变化,甚至有时候无法完成训练。更别提把在某个任务上 w…

1、linux中安装tomcat

1、创建目录 cd /opt ls mkdir tomcat 2、将文件拖入tomcat目录中 3、解压安装包 cd /opt/tomcat ls tar -zxvf apache-tomcat-8.5.59.tar.gz 4、启动tomcat cd /opt/tomcat/apache-tomcat-8.5.59/bin ./startup.sh 5、在linux中访问 http://localhost:8080/ 6、开放端口 …

为什么选择孟德尔随机化来写文章

为什么选择孟德尔随机化来写文章

【日常BUG】批量插入数据时报错: There is no setter for proerty named uptByd‘ in ‘class ...

前提&#xff1a;定义了自动填充策略&#xff0c;使用mybatis-plus 配置了属性自动注入 实体类上也定义了需要自动填充的字段。 但是在使用批量插入数据时报错&#xff1a; There is no setter for proerty named uptByd’ in class …&#xff0c; 排查过后发现是实体类中没…

QT QTableView添加CheckBox

需求&#xff1a;QTableView中指定列所有Item均为CheckBox项&#xff0c;要求CheckBox居中显示 显示效果如下&#xff1a;三种表现效果 实现方式&#xff1a; 系统控件QCheckBox 自绘制CheckBox CheckBox的图片 实现代码&#xff1a;(原理&#xff1a;利用委托自绘的方式)…

FasterViT实战:使用FasterViT实现图像分类任务(一)

文章目录 摘要安装包安装timm安装 grad-cam 数据增强Cutout和MixupEMA项目结构计算mean和std生成数据集 摘要 论文翻译&#xff1a;https://blog.csdn.net/m0_47867638/article/details/131542132 官方源码&#xff1a;https://github.com/NVlabs/FasterViT 这是一篇来自英伟…

Flask_使用flask_marshmallow序列化数据

代码如下&#xff1a; from flask import Flask from flask_marshmallow import Marshmallow from flask_sqlalchemy import SQLAlchemy from marshmallow import fieldsapp Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] "mysqlpymysql://root:12…

JavaFx 用户界面控件2——ListView

1.列表显示ListView 下面是一个JavaFX ListView的示例代码和使用方法&#xff1a; public class ListViewExample extends Application {Overridepublic void start(Stage primaryStage) {// 创建一个可观察的列表&#xff0c;用于存储ListView中的数据ObservableList<Str…

【深度学习笔记】正则化与 Dropout

本专栏是网易云课堂人工智能课程《神经网络与深度学习》的学习笔记&#xff0c;视频由网易云课堂与 deeplearning.ai 联合出品&#xff0c;主讲人是吴恩达 Andrew Ng 教授。感兴趣的网友可以观看网易云课堂的视频进行深入学习&#xff0c;视频的链接如下&#xff1a; 神经网络和…

如何手动初始化项目目录结构,并在命令行用gradle编译运行项目

父目录 Android 开发入门 - wuyujin1997 文章目录 Intro纯手动手动创建项目目录结构源码gradle tasksgradle wrapper执行前&#xff1a;执行后&#xff1a;执行前后对比&#xff1a; gradle wrappergradlew 和 gradlew.batplugin java编译Java项目【重点】如何通过 gradle run …

【GAMES202】Real-Time Shadows1—实时阴影1

一、Shadow Mapping回顾 [计算机图形学]光线追踪前瞻&#xff1a;阴影图(前瞻预习/复习回顾)__Yhisken的博客-CSDN博客 关于Shadow Mapping我们在GAMES101中学过&#xff0c;如果不知道可以参考我的博客。 Shadow Mapping是光栅化时为了实现阴影的一种算法&#xff0c;而它实…

python将dataframe数据导入MySQL

文章目录 mysql操作pymysql操作导入数据并启动使用pandas导入MySQL数据库连接引擎使用to_sql方法pandas读取sqlread_sql_tableread_sql_query mysql操作 创建数据库test和table create database test;CREATE TABLE car (文章ID int, 链接 VARCHAR(255), 标题 VARCHAR(255),发…