在安卓组件化项目中,理解和掌握Gradle的依赖传递和版本决议机制至关重要。这是因为组件化开发意味着将一个大型应用拆分成多个可独立构建、测试和复用的模块(组件),每个模块都有自己的依赖关系。
1、基本概念
dependencies {
//...
implementation 'com.google.android.material:material:1.8.0'
}
maven遵循着这样一个协议来保证唯一性,也就是GAV(坐标): groupId + artifactId + version 。
repositories {
google()
mavenCentral()
//远程maven仓库
maven { url 'https://jitpack.io' }
maven {
url uri('repo') //指定仓库为项目根目录的repo文件夹,也可以是本地任意目录
}
}
配置仓库地址如上,仓库地址可以是'jitpack.io' 这类远程仓库,也可以是发布到本地的本地仓库(一个路径)
2、依赖传递
| 方式 | 描述 |
|---|---|
| implementation | Gradle 会将依赖项添加到编译类路径,并将依赖项打包到 build 输出。不过,当您的模块配置 implementation 依赖项时,会让 Gradle 了解您不希望该模块在编译时将该依赖项泄露给其他模块。也就是说,其他模块只有在运行时才能使用该依赖项。使用此依赖项配置代替 api 或 compile(已废弃)可以显著缩短构建时间,因为这样可以减少构建系统需要重新编译的模块数。例如,如果 implementation 依赖项更改了其 API,Gradle 只会重新编译该依赖项以及直接依赖于它的模块。大多数应用和测试模块都应使用此配置。 |
| api | Gradle 会将依赖项添加到编译类路径和 build 输出。当一个模块包含 api 依赖项时,会让 Gradle 了解该模块要以传递方式将该依赖项导出到其他模块,以便这些模块在运行时和编译时都可以使用该依赖项。此配置的行为类似于 compile(现已废弃),但使用它时应格外小心,只能对您需要以传递方式导出到其他上游消费者的依赖项使用它。 这是因为,如果 api 依赖项更改了其外部 API,Gradle 会在编译时重新编译所有有权访问该依赖项的模块。 因此,拥有大量的 api 依赖项会显著增加构建时间。 除非要将依赖项的 API 公开给单独的模块,否则库模块应改用 implementation 依赖项。 |
| compile | Gradle 会将依赖项添加到编译类路径和 build 输出,并将依赖项导出到其他模块。此配置已废弃(在 AGP 1.0-4.2 中可用)。 |
| compileOnly | Gradle 只会将依赖项添加到编译类路径(也就是说,不会将其添加到 build 输出)。如果您创建 Android 模块时在编译期间需要相应依赖项,但它在运行时可有可无,此配置会很有用。如果您使用此配置,那么您的库模块必须包含一个运行时条件,用于检查是否提供了相应依赖项,然后适当地改变该模块的行为,以使该模块在未提供相应依赖项的情况下仍可正常运行。这样做不会添加不重要的瞬时依赖项,因而有助于减小最终应用的大小。 此配置的行为类似于 provided(现已废弃)。 |
| provided | Gradle 只会将依赖项添加到编译类路径(也就是说,不会将其添加到 build 输出)。此配置已废弃(在 AGP 1.0-4.2 中可用)。 |
| annotationProcessor | 如需添加对作为注解处理器的库的依赖,您必须使用 annotationProcessor 配置将其添加到注解处理器的类路径。这是因为,使用此配置可以将编译类路径与注解处理器类路径分开,从而提高 build 性能。如果 Gradle 在编译类路径上找到注解处理器,则会禁用避免编译功能,这样会对构建时间产生负面影响(Gradle 5.0 及更高版本会忽略在编译类路径上找到的注解处理器)。如果 JAR 文件包含以下文件,则 Android Gradle 插件会假定依赖项是注解处理器:META-INF/services/javax.annotation.processing.Processor如果插件检测到编译类路径上包含注解处理器,则会产生 build 错误。Kotlin使用kapt/ksp。 |
| testXxx | ... |
简单来说,implementation仅对当前模块可见,在编译期间不会传递给依赖此模块的其他模块。api不仅会被当前模块编译时使用,还会在编译期间暴露给所有依赖此模块的其他模块。compileOnly配置在Gradle中用于声明只在编译时需要的依赖,这些依赖不会被打包到最终的输出(如:JAR或APK),并且不会传递给其他模块作为运行时依赖。compile和provided已过时,被implementation和compileOnly替代。annotationProcessor配置注解处理器
3、版本决议
查看依赖关系比较常用的手段是打依赖树,即:
./gradlew app:dependencies
我们先在app>build.gradle里面依赖一个最新的正式版4.10.0,同时再依赖一个老版本4.9.3。
dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
}
sync之后执行`./gradlew app:dependencies
然后看看决策结果是多少,输出如下:
+--- com.squareup.okhttp3:okhttp: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.10 () | | --- org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31 -> 1.7.10 | --- org.jetbrains.kotlin:kotlin-stdlib:1.6.20 -> 1.7.10 () +--- com.squareup.okhttp3:okhttp:4.9.3 -> 4.10.0 (*)
我们多试几次可以得出如下结论:
| 分类 | 示例 | 决议结果 | 说明 |
|---|---|---|---|
| 全数字,段数不同 | 1.2.3 vs 1.4 | 1.4 | 段数依次比较,数字大的胜出 |
| 全数字,段数相同,位数相同 | 1.2.3 vs 1.2.4 | 1.2.4 | 同上 |
| 全数字,段数相同,位数不同 | 1.2.3 vs 1.2.10 | 1.2.10 | 同上 |
| 全数字,段数不同 | 1.2.3 vs 1.2.3.0 | 1.2.3.0 | 段数多的胜出 |
| 段数相同,字母比较 | 1.2.a vs 1.2.b | 1.2.b | 字母大的胜出 |
| 段数相同,数字与非数字 | 1.2.3 vs 1.2.abc | 1.2.3 | 数字优先字母 |
4、官方对版本决议规则的解释
第三条是我们自己经过试验得出的结论,gradle官方文档中已经对其版本决议规则进行了解释,**对规则的说明更详细,覆盖各种情况,**如下(机翻警告):
以下拷贝自docs.gradle.org/current/use…
版本具有隐式排序。版本排序用于:
- 确定特定版本是否包含在某个范围内。
- 在执行冲突解决时确定哪个版本是“最新的”(不过要注意,冲突解决使用 “基本版本”)。
版本根据以下规则排序:
- 每个版本都分为它的组成“部分”:
- 这些字符
[. - _ +]用于分隔版本的不同“部分”。 - 任何包含数字和字母的部分都被分成单独的部分:
1a1 == 1.a.1 - 仅比较版本的部分。实际的分隔符并不重要:(
1.a.1 == 1-a+1 == 1.a-1 == 1a1不过要注意,在解决冲突的上下文中,这条规则有例外)。
- 这些字符
- 使用以下规则比较 2 个版本的等效部分:
- 如果两个部分都是数字,则最高数值更高:
1.1<1.2 - 如果一个部分是数字,它被认为高于非数字部分:
1.a<1.1 - 如果两者都不是数字,则按字母顺序比较各部分,区分大小写:
1.A<1.B<1.a<1.b - 具有额外数字部分的版本被认为高于没有(即使它为零)的版本:
1.1<1.1.0 - 具有额外非数字部分的版本被认为低于没有以下版本的版本:
1.1.a<1.1
- 如果两个部分都是数字,则最高数值更高:
- 某些非数字部分在排序时具有特殊含义:
dev被认为低于任何其他非数字部分:1.0-dev<1.0-ALPHA<1.0-alpha<1.0-rc。- 字符串
rc,snapshot,final,ga,release和sp被认为高于任何其他字符串部分(按此顺序排序):1.0-zeta<1.0-rc<1.0-snapshot<1.0-final<1.0-ga<1.0-release<1.0-sp<1.0。 - 这些特殊值不区分大小写,与常规字符串部分相反,它们不依赖于它们周围使用的分隔符:
1.0-RC-1==`1.0.rc.1