1、前语

依靠是咱们在开发中最常用的装备,经过声明依靠项来引进项目所需技能,从而完成相关功能。

可是或许许多人都遇到过这种场景,编译运转后新增的接口或类找不到,又或许仅仅升级了一下某个Library,编译就提示找不到类或接口。这类问题在开发中是比较常见的,多数归于依靠版别抵触导致的,而在大型项目中,复杂度更高,这类问题呈现的频率也很高。

所以,搞清楚依靠装备,并能快速的处理依靠抵触,就变成开发中必不可少的技能了。

本文介绍要点:

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

2、依靠办理

为什么会有依靠办理这个东西呢?

这得回想到远古时期,那时分咱们的依靠是怎样做的,咱们需求先找到依靠,然后下载下来jar/aar,然后导进项目,再增加依靠装备,很繁琐,特别是在版别办理上更是如此,每次升级一个版别都要重复上述操作,保护成本巨大,开发同学叫苦连天。

而后有了maven。maven引进了规范依靠库对依靠进行办理,比远古时期的刀耕火种便利太多了,你只需保护好pom文件就完事了。Gradle在这方面其实跟maven很像,毕竟也是站在前辈的肩上。maven的pom跟Gradle的build.gradle文件很像,甚至能够说在思想上是一毛相同的,仅仅在写法上有些不同。

而现在咱们依据Gradle来开发,声明依靠项之后其实就不用管了,Gradle供给了很好的依靠办理支撑,Gradle自己会帮咱们去找到所需Library,主打的便是一个省心。

那么Gradle是怎样去找所需Library的呢?

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

在构建进程中,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" }
    }
}

大约是这么一个联系:

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

  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,第一条便是咱们要找的

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

然后点进去,并挑选一个版别

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

如上图,除了Library的基础信息之外,下方也介绍了不同构建工具是怎样声明依靠的。

当咱们增加好依靠之后,需求sync同步一下,然后Gradle就会依据你的装备去找依靠并增加到项目里了,sync完就能够在项目的External Libraries目录下找到它了。

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

回到方才的问题,作者是怎样确保自己的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表明。

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

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的版别都有哪些:

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

示例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。

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

咱们这儿用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的版别抉择来确保项目的编译运转。

不干涉的状况下,咱们项目里边就或许会存在一个库多个版别的状况。

比方:

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

所谓抉择机制,便是咱们面对多个版别、版别抵触时的处理方案。

一般处理方案会有如下几种。

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目标。

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

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

流程图:

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

两条线,分别对应着装备流程解析流程

3.4.4、额外一个小常识

假如你想对版别抵触的依靠项做版别办理,可是又不知道当时项目中有哪些依靠是重复的,从External Libraries里边一个一个的看又太费劲。

那么,我告知你一个小技巧,敞开版别抵触报错形式:

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

加上failOnVersionConflict()之后,编译解析的时分只需有重复的版别,也便是版别抵触的时分,就会直接报错,控制台会输出详细的依靠项和版别。

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

是不是很刺激…

4、总结

本文主要介绍了Gradle的依靠办理版别抉择

依靠办理里边需求重视的是依靠办法,不同的依靠办法抉择了是否会依靠传递;

版别抉择里边详细介绍了Gradle抉择规矩和版别号规矩,以及多种处理方案;

终究还有一个源码剖析和版别办理的小技巧。

总的来说,信息量仍是挺大的,记不住没联系,知道有这篇文章就行,用到了再回来看…

5、终究

催更的Gradle第6篇终于缓不济急,sorry~

假如本文或这个系列对你有收获,请不要吝啬你的支撑~

点重视,不迷路~

6、GitHub

github.com/yechaoa/Gra…

7、相关文档

  • Dependency management in Gradle
  • 增加 build 依靠项
  • Dependency Management Terminology
  • 读懂 gradle dependencies
  • Mastering Gradle
  • /post/699739…
  • The Java Platform Plugin
  • Declaring Versions and Ranges
  • Understanding dependency resolution
  • Controlling Transitive Dependencies

本文正在参加「金石方案」