【Gradle-6】一文搞懂Gradle的依赖管理和版本决议

1、前言

依赖是我们在开发中最常用的配置,通过声明依赖项来引入项目所需技术,从而实现相关功能。
但是可能很多人都遇到过这种场景,编译运行后新增的接口或类找不到,又或者只是升级了一下某个Library,编译就提示找不到类或接口。这类问题在开发中是比较常见的,多数属于依赖版本冲突导致的,而在大型项目中,复杂度更高,这类问题出现的频率也很高。
所以,搞清楚依赖配置,并能快速的解决依赖冲突,就变成开发中必不可少的技能了。

本文介绍重点:

2、依赖管理

为什么会有依赖管理这个东西呢?
这得回想到远古时期,那时候我们的依赖是怎么做的,我们需要先找到依赖,然后下载下来jar/aar,然后导进项目,再添加依赖配置,很繁琐,特别是在版本管理上更是如此,每次升级一个版本都要重复上述操作,维护成本巨大,开发同学叫苦不迭。
而后有了maven。maven引入了标准依赖库对依赖进行管理,比远古时期的刀耕火种方便太多了,你只要维护好pom文件就完事了。Gradle在这方面其实跟maven很像,毕竟也是站在前辈的肩上。maven的pom跟Gradle的build.gradle文件很像,甚至可以说在思想上是一毛一样的,只是在写法上有些不同。
而现在我们基于Gradle来开发,声明依赖项之后其实就不用管了,Gradle提供了很好的依赖管理支持,Gradle自己会帮我们去找到所需Library,主打的就是一个省心。

那么Gradle是如何去找所需Library的呢?
dependency-management-resolution.png
在构建过程中,Gradle首先会先从本地检索,找不到就挨个从远端仓库(中央仓库)找,找到之后会下载下来缓存到本地,默认缓存24h,可以加速下次构建,也避免了不必要的网络下载。

不要把依赖管理和版本管理搞混淆了。

2.1、声明依赖项

我们通常是在app > build.gradle > dependencies 中添加依赖:

dependencies {
    
    
    //...
    implementation 'com.google.android.material:material:1.8.0'
}

仓库配置:

pluginManagement {
    
    
    repositories {
    
    
        gradlePluginPortal()
        google()
        mavenCentral()
        // others 
    }
}

如果不是google、maven仓库的话,需要自己手动在repositories{ }里配置仓库地址,新建项目这俩默认就有了。
Gradle7.0之后,repositories{ }配置由build.gradle迁移到settings.gradle文件,如上。

2.1.1、依赖类型

plugins {
    
    
  id 'com.android.application'
}

android {
    
     ... }

dependencies {
    
    
    // Dependency on a local library module
    implementation project(':mylibrary')

    // Dependency on local binaries
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    // Dependency on a remote binary
    implementation 'com.example.android:app-magic:12.3'
}
  • 本地模块:需要在settings.gradle中include声明;
  • 本地二进制文件:需要在build.gradle声明路径;
  • 远端二进制文件:上述示例,也是用的最多的一种;

2.2、远端仓库

我们在repositories{ }里配置的url就是依赖项上传到远端仓库(中央仓库)的url,远端仓库起到一个桥梁作用,把开发者和作者连接起来。

pluginManagement {
    
    
    repositories {
    
    
        gradlePluginPortal()
        google()
        mavenCentral()
        // others 
        // maven { url "https://jitpack.io" }
    }
}

大概是这么一个关系:

  1. 左侧是我们的开发过程,通过配置声明依赖项和远端仓库地址,从而找到我们想要的Lib;
  2. 中间就是远端仓库,包含了丰富的Library/组件/插件等等;
  3. 右侧是其他开发作者,把代码通过aar/jar的方式打包上传到远端仓库提供给使用方;

这就是大概的流程,那么开发者如何找到自己想要的依赖呢,而作者又是如何保证自己的SDK被准备的找到呢?下面继续。

2.3、GAV

上面我们讲到通常是在app > build.gradle > dependencies 中添加依赖:

dependencies {
    
    
    //...
    implementation 'com.google.android.material:material:1.8.0'
}

上面这个是简写,全称是这样的:

implementation group: 'com.google.android.material', name: 'material', version: '1.8.0'

可以看到,信息比较全,但也不如第一种方式简洁,所以我们一般都是使用第一种方式来声明依赖项,并用冒号:分割。

那么有人会好奇了,我怎么知道有哪些Lib上传了远端仓库呢?
我们先想一下,这种依赖的远程库是在哪里的,常见的是发布在maven之类的仓库的对吧。
当我们清楚这一点之后,我们就可以去maven仓库去找我们要依赖的库,然后在库的信息页面,会有不同的依赖方式,比如maven、gradle、Ivy等等。
比如我们要找google官方的material库,除了在github仓库的readme文件找到声明方式之外,我们还可以在maven上去搜。
打开maven,搜索material,第一条就是我们要找的
在这里插入图片描述

然后点进去,并选择一个版本
1.8.0.png
如上图,除了Library的基础信息之外,下方也介绍了不同构建工具是如何声明依赖的。

当我们添加好依赖之后,需要sync同步一下,然后Gradle就会根据你的配置去找依赖并添加到项目里了,sync完就可以在项目的External Libraries目录下找到它了。
library.png

回到刚才的问题,作者是如何保证自己的Library被准备的找到呢?
这就跟app安装必须得是唯一id一样,有唯一性才能被准确定位,maven同样遵循着这样一个协议来保证唯一性,也就是GAV(坐标):groupId + artifactId + version

还是上面的maven信息页面,我们切到maven tab看看:

<!-- https://mvnrepository.com/artifact/com.google.android.material/material -->
<dependency>
    <groupId>com.google.android.material</groupId>
    <artifactId>material</artifactId>
    <version>1.8.0</version>
    <scope>runtime</scope>
</dependency>

通过maven的依赖方式可以清晰的看出GAV分别代表的是什么。

  • **groupId:**组织名称,一般是公司域名倒写,包名;
  • **artifactId:**项目名称,如果groupId包含了项目名称,这里就是子项目名称;
  • **version:**版本号,一般由3位数字组成(x.y.z);

这样,通过GAV我们就可以准确的找到一个Library了,不同的是在Gradle的声明中,artifactId用name表示。

2.4、依赖传递

Gradle除了会帮我们下载依赖之外,还提供了依赖传递的能力。试想一下上面远古时期的操作,如果另一个项目也需要同样的依赖,是不是就得copy一份了,又是繁琐++。
Gradle的依赖传递其实对应着maven里面的Scope,比如我们常用的implementation、api,不同的依赖方式决定了依赖传递的不同效果,搞不清楚这个,也会经常遇到编译问题,不知道怎么解决。

2.4.1、依赖方式

方式 描述
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(compile),implementation支持依赖关系颗粒度更细的范围界定,而api(compile)与之相反,具有依赖传递性,这不仅会影响编译速度,更严重的是,依赖传递会出现版本冲突,比如你用的Kotlin版本是1.5,依赖了一个三方库的Kotlin版本是1.8,然后这个1.8的版本就跟你的项目各种不兼容,比如出现找不到类、接口、函数等情况,就会出现编译错误。
所以下面介绍Gradle是如果做版本决议的,以及保证版本一致性、可用性的一些解决方案。

3、版本决议

当我们项目出现版本冲突的时候,要先能定位问题,然后才是解决问题。

3.1、依赖信息

定位问题一般会从依赖关系下手。
查看依赖关系比较常用的手段是打依赖树,即:

./gradlew app:dependencies

除了cli命令之外,还可以使用build --scan,或者AS右上角的Gradle>app>help>dependencies,点击执行也可。

执行结果如下:

+--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10
|    +--- org.jetbrains.kotlin:kotlin-stdlib:1.7.10
|    |    +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10
|    |    \--- org.jetbrains:annotations:13.0
|    \--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10
|         \--- org.jetbrains.kotlin:kotlin-stdlib:1.7.10 (*)
+--- androidx.core:core-ktx:1.7.0
|    +--- org.jetbrains.kotlin:kotlin-stdlib:1.5.31 -> 1.7.10 (*)
|    +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
|    \--- androidx.core:core:1.7.0 -> 1.8.0
|         +--- androidx.annotation:annotation:1.2.0 -> 1.3.0
|         +--- androidx.annotation:annotation-experimental:1.1.0
|         +--- androidx.lifecycle:lifecycle-runtime:2.3.1 -> 2.5.0
|         |    +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
|         |    +--- androidx.arch.core:core-common:2.1.0
|         |    |    \--- androidx.annotation:annotation:1.1.0 -> 1.3.0
|         |    \--- androidx.lifecycle:lifecycle-common:2.5.0
|         |         \--- androidx.annotation:annotation:1.1.0 -> 1.3.0
|         \--- androidx.versionedparcelable:versionedparcelable:1.1.1
|              +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
|              \--- androidx.collection:collection:1.0.0 -> 1.1.0
|                   \--- androidx.annotation:annotation:1.1.0 -> 1.3.0
...

这个里面包含了所有的依赖信息,比如A引入了B,B引入了C,C的版本在哪被拉高的等等。

一般为了方便查看和搜索,我会选择输出到文件,即:./gradlew app:dependencies > dependencies.txt

这个依赖树的信息怎么看呢,简单介绍一下:
首先它是一个树状的结构来表示依赖的信息,一级就是项目里面依赖的配置,属于直接依赖,比如core-ktx,kotlin-stdlib-jdk8虽然不是dependencies{ }里面配置的,但是由kotlin plugin引进来的,跟kotlin plugin的版本也能对应上,也算是直接依赖。

然后看直接依赖的下一级甚至下下级,都是直接依赖的库所依赖的,也可以说是引进来的。往往这部分我们其实是没有感知的,也比较容易被忽略,而恰恰这部分引入的库是很有可能出问题的。
比如这个:

+--- androidx.core:core-ktx:1.7.0
|    +--- org.jetbrains.kotlin:kotlin-stdlib:1.5.31 -> 1.7.10 (*)

它表示core-ktx中依赖的kt标准库由1.5.31被拉高到1.7.10了。

最后是看依赖项的版本信息,比如:1.5.31 -> 1.7.10 (*)。
版本信息正常应该是这样的:

androidx.activity:activity:1.5.0

不正常的就有多样了:

androidx.annotation:annotation:1.1.0 -> 1.3.0

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

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

androidx.test:core:{
    
    strictly 1.4.0} -> 1.4.0 (c)
  • ->:表示冲突,比如这个1.1.0 -> 1.3.0,-> 后面的版本表示Gradle决议之后的版本,这里表示1.1.0版本被拉高到1.3.0;
  • **其实是省略的意思,层级太深,Gradle就省略了一部分,而越深的信息也不太重要,就显的冗余,往往重要的信息都在前几层;
  • c:c是constraints的简称,主要是用来保证当前依赖项所需要的依赖的版本的一致性,白话讲就是为了防止其他依赖项把我需要的依赖给拉高而导致我自己不可用的情况。
  • strictly:strictly跟force一样表示强制使用该版本,区别在于strictly可以在依赖树里标示出来,而force则没有任何标示,所以force在高版本里也被废弃了。

3.2、决议规则

版本决议是指在某个依赖出现多个版本的时候(版本冲突),Gradle如何选择最终的版本来参与编译。
版本决议这个空讲不如上代码来的直接,我们就用常用的网络库okhttp来举例吧。
我们先去maven上搜一下okhttp的版本都有哪些:
okhttp.png

示例1:

我们先在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 > dependencies.txt
然后看看决策结果是多少,输出如下:

+--- 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:

新建一个名为plugin的Module,来模拟多Module情况下版本冲突的场景。
在plugin Module里依赖okhttp4.9.3,在app Module里依赖4.10.0;
plugin>build.gradle:

dependencies {
    implementation 'com.squareup.okhttp3:okhttp:4.9.3'
}

app>build.gradle:

dependencies {
    implementation 'com.squareup.okhttp3:okhttp:4.10.0'
}

运行,输出如下:

+--- com.squareup.okhttp3:okhttp:{strictly 4.10.0} -> 4.10.0 (c)

然后反过来,在plugin Module里依赖okhttp4.10.0,在app Module里依赖4.9.3
运行,输出如下:

+--- com.squareup.okhttp3:okhttp:{strictly 4.9.3} -> 4.9.3 (c)

结论2:

多个模块的多个相同依赖,优先选择主模块(app)的版本,并默认有strictly关键字约束;

示例3:

在plugin Module里强制依赖okhttp4.9.3,在app Module里依赖4.10.0。
force.png
我们这里用force的话可以看到被废弃了,源码让我们用strictly代替:

    /**
     * Sets whether or not the version of this dependency should be enforced in the case of version conflicts.
     *
     * @param force Whether to force this version or not.
     * @return this
     *
     * @deprecated Use {@link MutableVersionConstraint#strictly(String) instead.}
     */
    @Deprecated
    ExternalDependency setForce(boolean force);

那我们用strictly改一下:

    implementation('com.squareup.okhttp3:okhttp') {
        version{
            strictly("4.9.3")
        }
    }

运行,输出如下:

+--- com.squareup.okhttp3:okhttp:{strictly 4.10.0} -> 4.10.0 (c)

可以看到,plugin Module的强制版本4.9.3没起到效果。
那么我们反过来,在app Module里强制依赖okhttp4.9.3,在plugin Module里依赖4.10.0试试;
app>build.gradle:

    implementation('com.squareup.okhttp3:okhttp') {
        version{
            strictly("4.9.3")
        }
    }

plugin>build.gradle:

implementation 'com.squareup.okhttp3:okhttp:4.10.0'

运行,输出如下:

+--- com.squareup.okhttp3:okhttp:{strictly 4.9.3} -> 4.9.3

结论3:

因为默认有strictly关键字的约束,所以子模块的强制版本是失效的,即使子模块的版本比app模块的版本高,也优先选择主模块(app)中依赖的版本。虽然版本降级的情况少见,但这也不失为一种解决方案…

ps:如果上面strictly的用法你觉得有些繁琐,也可以选择用!!简写的方式代替:

implementation 'com.squareup.okhttp3:okhttp:4.10.0!!'

示例4:

在app 同时依赖okhttp4.10.0和5.0.0-alpha.11,看看如何决议

dependencies {

    implementation 'com.squareup.okhttp3:okhttp:4.10.0'

    implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
}

运行,输出如下:

+--- com.squareup.okhttp3:okhttp:4.10.0 -> 5.0.0-alpha.11

结论4:

虽然版本号带字母,但是前面的基础版本5.0.0高于4.10.0,所以选择5.0.0-alpha.11,修饰词其次;

示例5:

在app 同时依赖okhttp4.10.0和5.0.0-alpha.11,但4.10.0版本使用force强制依赖版本

dependencies {

    implementation( 'com.squareup.okhttp3:okhttp:4.10.0'){
        force(true)
    }

    implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
}

运行,输出如下:

+--- com.squareup.okhttp3:okhttp:5.0.0-alpha.11 -> 4.10.0

可以看到,版本是被拉低了的。

那给5.0.0-alpha.11版本用strictly的方式强制一下试试

implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11!!'

运行,输出如下:

+--- com.squareup.okhttp3:okhttp:4.10.0 FAILED
+--- com.squareup.okhttp3:okhttp:{strictly 5.0.0-alpha.11} FAILED

可以看到报错了,并且在External Libraries里面找不到okhttp的依赖。

结论5:

force优先级高于strictly,如果二者同时显式声明,则会报错。

示例6:

在app 同时依赖okhttp4.10.0和5.0.0-alpha.11,并同时都使用force强制依赖版本

dependencies {

    implementation( 'com.squareup.okhttp3:okhttp:4.10.0'){
        force(true)
    }
    
    implementation( 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'){
        force(true)
    }
}

运行,输出如下:

+--- com.squareup.okhttp3:okhttp:5.0.0-alpha.11 -> 4.10.0 (*)

4.10.0和5.0.0-alpha.11版本的依赖顺序换一下试试

dependencies {

    implementation( 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'){
        force(true)
    }
    
    implementation( 'com.squareup.okhttp3:okhttp:4.10.0'){
        force(true)
    }
}

运行,输出如下:

+--- com.squareup.okhttp3:okhttp:4.10.0 -> 5.0.0-alpha.11 (*)

结论6:

同时使用force强制依赖版本时,版本决议的结果跟依赖顺序有关,最早force的版本优先。

示例7:

模拟一个三方库依赖传递的场景。
Android开发的应该都知道retrofit,而retrofit也依赖了okhttp,那我们把retrofit也引进来看看

dependencies {

    implementation 'com.squareup.okhttp3:okhttp:4.10.0'
    
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
}

运行,输出如下:

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

可以看到,retrofit中依赖的okhttp3.14.9被拉高到4.10.0了。

那么现在,我们去掉项目中依赖的okhttp4.10.0,然后再依赖一个更低版本的retrofit,看看okhttp的版本是多少

dependencies {

//    implementation 'com.squareup.okhttp3:okhttp:4.10.0'

    implementation 'com.squareup.retrofit2:retrofit:2.9.0'

    implementation 'com.squareup.retrofit2:retrofit:2.0.0'
}

运行,输出如下:

+--- com.squareup.retrofit2:retrofit:2.0.0 -> 2.9.0 (*)

可以看到,retrofit的版本2.0.0拉高到2.9.0,并且没有子级。

结论7:

当项目中的依赖与三方库中的依赖相同时,优先选择高版本;
当多个三方库参与版本决议时,优先高版本,且子级跟随父级,即选择一级依赖版本决议结果的子级;

总结

ok,示例了这么多,我们再把结论总结一下:

  1. 当有多个相同依赖时,不管是哪里引入的,gradle总会优先选择最高版本;
  2. 当多个相同依赖没有版本约束条件时,优先选择主模块(app)中的版本,且默认有strictly约束版本;
  3. force优先级高于strictly,如果二者同时显式声明,则会报错,推荐使用strictly;
  4. 同时使用force强制依赖版本时,版本决议的结果跟依赖顺序有关,最早force的版本优先;

3.3、版本号规则

分类 示例 决议结果 说明
全数字,段数不同 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 数字优先字母

Gradle也支持版本号的范围选择,比如[1.0,)、[1.1, 2.0)、(1.2, 1.5]、1.+、latest.release等,但是这种一般很少用,感兴趣的可以去看Gradle文档,或maven文档

3.4、解决冲突

当项目复杂到一定程度的时候(依赖多),很多依赖传递就变得不可控了,随之而来的就是各种依赖版本冲突。不管是主工程的模式也好,还是单独搞个模块管理依赖,我们都需要有一个决议机制,用来保证依赖版本全局的唯一性、可用性。
此外,因为Gradle版本决议的默认规则是选择最高的版本,但是最高的版本很有可能是与项目不兼容的,所以这时候我们就要去干预Gradle的版本决议来保证项目的编译运行。
不干预的情况下,我们项目里面就可能会存在一个库多个版本的情况。
比如:
多个版本.png

所谓决议机制,就是我们面对多个版本、版本冲突时的解决方案。
一般解决方案会有如下几种。

3.4.1、版本管理

解决冲突最好的办法就是避免冲突。
尽管版本管理在项目初期可以做的非常好,但是在项目和开发人员的双重迭代下,劣化只是时间的问题而已,所以建议在项目初期就做好版本管理的规划,因为这玩意儿越往后,真的越难改,也不是能力问题,主要是投入产出比实在是不高。
那么问题来了,版本管理有哪些方式呢?

  1. 早期的方案是新建一个或多个.gradle文件来做依赖和版本的双重管理,比如version.gradle;
  2. 后来新建项目就会有默认的ext { }了,属于是官方在版本管理上又迈了一步;
  3. 再后来就是buildSrc了,相比于ext,buildSrc可以把依赖和版本都单独的抽出去,支持提示和跳转算是它的最大优势了;
  4. 最新的就是Gradle7.0以后的Catalog了,“对所有module可见,可统一管理所有module的依赖,支持在项目间共享依赖”;
  5. 其实这中间还有一个很多人不知道的东西,java-platform插件,准确的说它属于依赖管理,也包含了版本管理,也支持多项目共享;

大概介绍这些,有机会的话再展开吧…

如果说版本管理是提前规划,那下面的操作就属于后期人为干预了。

3.4.2、强制版本

如果没有版本管理,或者版本管理的能力比较弱,那就只能强制版本了。
强制版本分两部分,一是修改依赖配置添加版本约束,二是编译期修改版本决议规则。

当我们使用依赖配置进行版本约束时,形式如下:

    implementation('com.squareup.okhttp3:okhttp:4.10.0') {
        force(true)
    }

那我们如何知道implementation后面可以跟哪些约束呢,这些约束又是代表什么意思呢?
implementation本质上是添加依赖嘛,依赖项配置对应的就是Dependency对象,它在dependencies { }中对应的其实是多个集合,也就是多个依赖集合,对应不同的依赖形式,比如implementation、testImplementation、fileXXX等。
既然依赖项配置对应的就是Dependency对象,那支持哪些约束条件,就在这个类及其子类里。
我翻了源码,总结了一下Dependency及其子类下提供的常用的约束条件:

  • ExternalDependency > setForce:版本冲突的情况下,是否强制此依赖项的版本。
  • ExternalDependency > version:配置此依赖项的版本约束。是一个闭包,其下可接收strictly、require、prefer、reject。
  • ModuleDependency > exclude:通过排除规则,来排除此依赖的可传递性依赖。
  • ModuleDependency > setTransitive:是否排除当前依赖里包含的可传递依赖项。
  • ExternalModuleDependency > setChanging:设置Gradle是否始终检查远程仓库中的更改。常用于快照版本SNAPSHOT的变更检查,因为Gradle默认会有缓存机制(默认24h),而SNAPSHOT版本的变更相对更频繁一些。或者使用resolutionStrategy提供的cacheChangingModulesFor(0, 'SECONDS')来设置缓存时长(check for updates every build)。

下面再来分别简单介绍一下。

3.4.2.1、force

版本冲突的情况下,是否强制此依赖项的版本。
虽然Gradle已经开启8.0时代了,但是使用老版本的项目依然有很多,所以使用force强制版本的方式依然可用。
force的结果跟依赖顺序有关,最早force的版本优先。

    implementation('com.squareup.okhttp3:okhttp:4.10.0') {
        force(true)
      	// or
      	// force = true
    }

3.4.2.2、strictly

声明强制版本,上面我们演示过了,高版本中默认就有strictly的隐式声明,如果显式声明的版本无法解析,编译期会报错。代替force的新方式,推荐使用。

    implementation 'com.squareup.okhttp3:okhttp:4.10.0!!'

  	// or

    implementation('com.squareup.okhttp3:okhttp') {
        version{
            strictly("4.10.0")
        }
    }

3.4.2.3、exclude

通过排除规则,来排除此依赖的可传递性依赖。
排除规则(还是基于GAV):

  • group
  • module
  • group + module

比如排除retrofit里面自带的okhttp:

    implementation('com.squareup.retrofit2:retrofit:2.9.0') {
        exclude(group: "com.squareup.okhttp3", module: "okhttp")
    }

排除前:

+--- 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.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.retrofit2:retrofit:2.9.0

慎用,因为你不确定排除后原来依赖是否还正常可用,比如retrofit就是需要okhttp,你给干掉了,不就G了吗…

3.4.2.4、transitive

是否排除当前依赖里包含的可传递依赖项。

  • false:不传递
  • true:传递
    implementation('com.squareup.retrofit2:retrofit:2.9.0') {
        transitive(false)
    }

3.4.2.5、configurations

基于Gradle生命周期hook的后置操作,算是终极方案,也是目前比较有效的解决方案。

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.squareup.okhttp3' && requested.name == 'okhttp') {
            details.useVersion '4.10.0'
        }
    }
}

details.useVersion ‘4.10.0’ 这里的版本号也支持gradle.properties中定义的变量,比如:

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.yechaoa.plugin' && requested.name == 'plugin') {
            details.useVersion PLUGIN_VERSION
        }
    }
}

或者我们也可以直接force某个具体的依赖项

configurations.all {
    resolutionStrategy.force 'com.squareup.okhttp3:okhttp:4.10.0'

  	// or

  	resolutionStrategy {
  			force('com.squareup.okhttp3:okhttp:4.10.0')
    }
}

上面的代码可能有的同学搜到过,但好像没人分析过,因为是比较有效的解决方案,我姑且从源码的阶段来分析一下。

3.4.3、源码分析

我们前文(【Gradle-4】Gradle的生命周期)讲到的声明周期的第二阶段Configuration,Gradle会去解析build.gradle配置生成Project对象。
依赖配置的闭包dependencies { } 其实调用的就是Project对象的dependencies(Closure configureClosure)方法,dependencies()方法接收一个闭包对象,这个闭包就是我们的配置项。
然后这个闭包通过DependencyHandler对象代理解析给Project,但也不是直接解析,这中间还涉及到一些操作,DependencyHandler会把依赖项分组到Configuration中。

那Configuration又是个什么东西?
Configuration表示一组dependencies,也就是Dependency集合。

为什么是个集合?
因为对应不同的依赖形式,比如implementation、testImplementation、fileXXX等,也就是说对应着不同的Configuration对象,所以,一个项目有多个Project对象,一个Project对象有多个Configuration对象。

ok,回到hook生命周期的问题上来。
我们配置依赖项是在dependencies { } 中配置的,但是解析是在编译时做的对吧。
那么再次回溯下我们的诉求,要在编译期把版本给强制了。
Gradle生命周期有三个阶段,初始化、配置、执行,执行阶段肯定是不行了,而配置阶段正好是解析build.gradle文件的时候,那么,我们就可以在解析完build.gradle之后,再去找到我们需要强制版本的依赖项,然后去强制版本。
ok,思路清晰了,那么就是开搞!
前面提到我们的依赖配置项dependencies { }解析完就是Project对象下的多个Configuration对象对吧,所以我们就需要找到Project对象下所有的Configuration对象,既然Configuration对象有多个,肯定得有个容器吧,确实有,就是ConfigurationContainer,就是负责管理Configuration的。
Project对象也提供了获取所有的Configuration对象的方法,就是getConfigurations(),返回一个ConfigurationContainer对象,

public interface Project extends Comparable<Project>, ExtensionAware, PluginAware {
  	// ...

		ConfigurationContainer getConfigurations();

		// ...
}

当我们拿到所有的Configuration对象之后,就是遍历Configuration了。
而Configuration对象其实已经给我们提供了一个解析策略,就是ResolutionStrategy对象,
ResolutionStrategy对象就是专门用来处理依赖关系的,比如强制某些依赖版本、替换、解决冲突或快照版本超时等。
所以,遍历Configuration之后,就是获取ResolutionStrategy对象,然后继续遍历,获取我们具体的依赖项。
我们具体的依赖项配置的时候是这样的:

implementation 'com.squareup.retrofit2:retrofit:2.9.0'

但是解析之后是由DependencyResolveDetails对象承载的,但是它其实是一个中间层,具体的接收对象是ModuleVersionSelector对象,

public interface ModuleVersionSelector {

    String getGroup();

    String getName();

    @Nullable
    String getVersion();

    boolean matchesStrictly(ModuleVersionIdentifier identifier);

    ModuleIdentifier getModule();
}

通过ModuleVersionSelector对象,我们可以获取Group、Name、Version,这就对应着我们前面讲到的GAV。
那么中间层DependencyResolveDetails对象是干嘛的呢,DependencyResolveDetails对象除了获取原始数据之外,提供了解决版本冲突的方法,比如useVersion、useTarget,这个我们在前文生命周期的插件管理小节上提到过,与PluginResolveDetails同出一辙。

所以,最终就有了如下的代码:

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.squareup.okhttp3' && requested.name == 'okhttp') {
            details.useVersion '4.10.0'
        }
    }
}

再来分析下这段代码:

  1. 首先获取Project对象下所有的Configuration对象,即configurations
  2. 然后遍历所有的Configuration对象,即all
  3. 然后获取Configuration对象提供的专门处理依赖关系的ResolutionStrategy对象,即resolutionStrategy
  4. 然后遍历Configuration下所有的依赖项,即eachDependency
  5. 然后获取具体的某个依赖项,接收对象是ModuleVersionSelector,即details.requested
  6. 然后进行条件匹配,即group == 、name ==
  7. 最后,匹配成功,就使用DependencyResolveDetails对象提供的方法进行强制版本,即details.useVersion

流程图:

两条线,分别对应着配置流程解析流程

3.4.4、额外一个小知识

如果你想对版本冲突的依赖项做版本管理,但是又不知道当前项目中有哪些依赖是重复的,从External Libraries里面一个一个的看又太费劲。
那么,我告诉你一个小技巧,开启版本冲突报错模式:

configurations.all {
    resolutionStrategy{
        failOnVersionConflict()
    }
    // ...
}

加上failOnVersionConflict()之后,编译解析的时候只要有重复的版本,也就是版本冲突的时候,就会直接报错,控制台会输出具体的依赖项和版本。
fialonversion.png
是不是很刺激…

4、总结

本文主要介绍了Gradle的依赖管理版本决议
依赖管理里面需要关注的是依赖方式,不同的依赖方式决定了是否会依赖传递;
版本决议里面具体介绍了Gradle决议规则和版本号规则,以及多种解决方案;
最后还有一个源码分析和版本管理的小技巧。
总的来说,信息量还是挺大的,记不住没关系,知道有这篇文章就行,用到了再回来看…

5、最后

催更的Gradle第6篇终于姗姗来迟,sorry~
如果本文或这个系列对你有收获,请不要吝啬你的支持~
点关注,不迷路~

6、GitHub

https://github.com/yechaoa/GradleX

7、相关文档

猜你喜欢

转载自blog.csdn.net/yechaoa/article/details/130445269
今日推荐