文章目录
- 1. 传递依赖的概念
- 2. 传递依赖的好处
- 3. 传递依赖的规则
- 4. 依赖图与层级结构
- 5. 依赖层级和作用域
- 6. 依赖冲突的原因
- 7. 传递依赖带来的版本冲突
- 8. 冲突解决策略
- 1. 默认的最高版本策略
- 2. 查看已解决的依赖冲突
- 3. 强制指定版本 (force = true)
- 4. 排除特定依赖 (exclude)
- 5. 修复依赖子集
- 6. 依赖替换规则
- 7. 失败快速策略
- 9. 依赖约束 (Constraints)
本文主要解释Gradle中的依赖传递与冲突解决。
传递依赖是 Gradle 依赖管理系统的核心功能之一,它允许项目自动获取直接依赖项所需的所有依赖项,而无需手动声明每一个依赖。
1. 传递依赖的概念
传递依赖定义:当 A 依赖 B,B 依赖 C 时,A 会自动获得对 C 的依赖。
我们来个图:
Project ---> Library A ---> Library B| |v vLibrary C Library D|vLibrary E
在上面的例子中,如果您的项目依赖 Library A,您会自动获得 Libraries B, C, D, 和 E 作为传递依赖。
2. 传递依赖的好处
- 简化依赖声明:只需声明直接依赖,而不是所有级联依赖
- 保持依赖一致:确保依赖库使用其兼容的版本
- 减少版本管理负担:库的作者可以管理其内部依赖关系
3. 传递依赖的规则
-
依赖配置类型
api
/compile
: 完全传递依赖(包括编译时和运行时)implementation
: 仅运行时传递,编译时不传递compileOnly
: 不传递runtimeOnly
: 仅运行时传递 -
依赖版本:当同一库的多个版本出现在依赖图中时,Gradle 默认使用最高版本策略解决冲突
-
传递性标志:可以通过
transitive=false
参数禁用特定依赖的传递性
4. 依赖图与层级结构
依赖图(Dependency Graph)是所有依赖项及其关系的可视化表示。在复杂项目中,这个图可能非常庞大。
我们可以使用以下命令查看项目中的依赖图:
# 查看主要依赖图
./gradlew dependencies# 查看特定配置的依赖图
./gradlew dependencies --configuration implementation# 查看特定模块的依赖图
./gradlew :app:dependencies
来个输出实例:
+--- androidx.databinding:databinding-runtime:7.0.4
| +--- androidx.databinding:databinding-common:7.0.4
| +--- androidx.databinding:viewbinding:7.0.4 (*)
| \--- androidx.lifecycle:lifecycle-runtime:2.2.0 -> 2.3.1
| +--- androidx.lifecycle:lifecycle-common:2.3.1
| | \--- androidx.annotation:annotation:1.1.0 -> 1.2.0
| +--- androidx.arch.core:core-common:2.1.0
| | \--- androidx.annotation:annotation:1.1.0 -> 1.2.0
| \--- androidx.annotation:annotation:1.1.0 -> 1.2.0
5. 依赖层级和作用域
在 Gradle 中,依赖的作用域(可见性)取决于声明依赖所用的配置(implementation、api 等),依赖链中的每个库所使用的配置 和 依赖链的长度(依赖层级)。
我们来个例子,假如存在以下的依赖链:
App ---(implementation)---> Library A ---(api)---> Library B
- Library B 对 App 的编译时不可见(因为 App 使用 implementation 引入 Library A)
- 但 Library B 对 App 的运行时可见
然而对于
App ---(api)---> Library A ---(implementation)---> Library B
- Library B 对 App 的编译时和运行时都不可见(因为 Library A 使用 implementation 引入 Library B)
6. 依赖冲突的原因
依赖冲突是大型项目中常见的问题,当项目依赖图中包含同一个库的多个不同版本时就会发生。我们可以来一些经典的冲突场景:
-
直接依赖不同版本
dependencies {implementation 'com.google.code.gson:gson:2.8.9' // 我们直接请求的版本implementation 'com.some.library:library:1.0.0' // 此库内部依赖 gson:2.8.6 }
-
不同库依赖同一个库的不同版本
App ---> Library A ---> commons-io:2.6|+---> Library B ---> commons-io:2.8
-
菱形依赖
App/ \/ \Library A Library B\ /\ /Gson(v2.7.2 vs v2.8.5)
在这种情况下,App 依赖 Library A 和 Library B,而它们都依赖 commons-io 但版本不同。
7. 传递依赖带来的版本冲突
传递依赖可能导致深度嵌套的版本冲突,这些冲突在项目依赖树的不同分支之间发生,可能难以追踪和解决。
我们可以来一些常见的问题:
- 运行时错误:不兼容的库版本导致类似
NoSuchMethodError
等异常 - 行为不一致:不同版本库的行为差异导致难以追踪的错误
- 肥胖JAR:多个版本的同一个库被打包,导致构建产物体积膨胀
8. 冲突解决策略
好在Gradle提供了多种机制来解决依赖冲突。我们来聊一下具体如何进行解决的。
1. 默认的最高版本策略
默认情况下,Gradle 使用"最高版本策略"(highest version strategy)解决冲突。在依赖冲突时,Gradle 会检测依赖图中同一库的所有版本,选择版本号最高的,然后用这个版本替换所有其他版本。
例如:如果依赖图包含 commons-io:commons-io:2.6
和 commons-io:commons-io:2.8.0
,Gradle 会选择 2.8.0 版本。这个默认的策略基于这样的假设:更高版本通常向后兼容,但这并不总是正确的。
2. 查看已解决的依赖冲突
我们可以通过添加 --warning-mode all
标志查看 Gradle 自动解决的依赖冲突:
./gradlew dependencies --warning-mode all
输出实例为:
The following dependencies were resolved with conflict resolution:commons-io:commons-io:2.6 -> 2.8.0org.apache.httpcomponents:httpclient:4.5.10 -> 4.5.13
3. 强制指定版本 (force = true)
当默认策略不适用时,可以强制使用特定版本:
dependencies {// 方式1:在特定依赖上设置force标志implementation('commons-io:commons-io:2.6') {force = true // 强制使用2.6版本,即使依赖图中有更高版本}// 方式2:通过resolutionStrategy全局强制configurations.all {resolutionStrategy {force 'commons-io:commons-io:2.6'force 'org.apache.httpcomponents:httpclient:4.5.10'}}
}
在Kotlin DSL中:
dependencies {implementation("commons-io:commons-io:2.6") {isForce = true}configurations.all {resolutionStrategy {force("commons-io:commons-io:2.6")}}
}
4. 排除特定依赖 (exclude)
如果某个传递依赖导致问题,可以完全排除它:
dependencies {// 方式1:在特定依赖上排除implementation('com.some.library:library:1.0.0') {// 排除此库的所有commons-io传递依赖exclude group: 'commons-io', module: 'commons-io'}// 方式2:配置级别排除configurations.implementation {exclude group: 'commons-io'}// 排除后,如果需要,可以添加自己指定的版本implementation 'commons-io:commons-io:2.6'
}
排除可以指定:
- 只有
group
:排除该组的所有模块 - 只有
module
:排除所有组中的此模块 - 同时指定
group
和module
:排除特定组中的特定模块
5. 修复依赖子集
有时候,我们可能希望将一组相关的库固定在同一版本上:
configurations.all {resolutionStrategy {// Spring 组件保持相同版本eachDependency { DependencyResolveDetails details ->if (details.requested.group == 'org.springframework') {details.useVersion '5.3.10'}}}
}
6. 依赖替换规则
我们可以通过 ResolutionStrategy
创建更复杂的替换规则:
configurations.all {resolutionStrategy {// 将所有com.old.library依赖替换为com.new.librarydependencySubstitution {substitute module('com.old.library:library') using module('com.new.library:library:1.0.0')}// 有条件替换eachDependency { DependencyResolveDetails details ->if (details.requested.group == 'org.apache.commons' && details.requested.name == 'commons-lang3' && details.requested.version < '3.9') {details.useVersion '3.9'}}}
}
7. 失败快速策略
有时,最好在依赖冲突发生时立即失败,而不是自动解决它们:
configurations.all {resolutionStrategy {// 冲突时立即失败failOnVersionConflict()}
}
这有助于发现潜在的兼容性问题,而不是依赖自动解决机制。
9. 依赖约束 (Constraints)
为了更优雅的解决冲突问题,我们可以引入依赖约束。依赖约束允许我们为传递依赖指定版本,即使没有直接依赖它,并且为可能添加的依赖预先定义版本,而且还能在多个模块之间协调传递依赖的版本。我们可以来个例子:
dependencies {// 正常依赖implementation 'org.springframework:spring-web:5.3.10'// 约束定义constraints {// 强制所有httpclient的实例使用这个版本,// 即使我们没有直接依赖它,但它可能作为传递依赖出现implementation 'org.apache.httpcomponents:httpclient:4.5.13'// 预定义版本,以便稍后使用implementation 'com.google.code.gson:gson:2.8.9'}
}
依赖约束特别适合在多模块项目统一依赖版本:
// 在父项目或平台项目中定义约束
dependencies {constraints {api 'org.apache.commons:commons-lang3:3.12.0'api 'com.google.guava:guava:31.0.1-jre'}
}// 在子模块中,无需指定版本
dependencies {implementation 'org.apache.commons:commons-lang3' // 将使用约束中的版本
}
在解决冲突时,约束的应用顺序如下:
- 强制版本(force = true)
- 直接依赖定义的版本
- 约束中定义的版本
- 传递依赖的版本(默认选择最高版本)