Gradle 爬坑指南 -- 依赖管理

7,366 阅读22分钟

书接上文:

前面的内容算是把 Gradle 基本的概念讲清楚了,要想用好 Gradle 这个工具、框架,单靠这些内容远远不够,有些内容还需要大家继续深入研究、学习。这部分内容才是实际项目中 莫名其妙、怎么搞不懂 的真正所在。本文对这部分内容也是尽量深入一点,剩下的还是要靠大家自己,多找找资料丰富自己

依赖分析工具

先介绍几个命令行分析工具,我觉得很有必要从这里开始

1. Profile 命令

gradle assembleDebug --profile 命令可以分析项目构建过程,可以看性能、可以看耗时、构建每个阶段、方法耗时清清楚楚,还可以看依赖版本统计。虽然 AS 在构建完毕之后有蓝字提示,点击后可以看 AS 对构建性能的分析,但是哪有这个工具来的清晰

Profile 工具会在 根目录/build/reports/profile 中生成 HTML 格式的分析文件的分析文件 ,内容清晰大方、方便查看、方便截图、截取内容做文档用

上面几个 tab 就不说了,大家都看得懂,Task Execution 会统计 Task 总的耗时。这里要注意,Gradle 在 Task 执行阶段是 多线程并行模式,最多8个线程 ,所以统计的总时间是每个线程耗时之和

assembleDebug 是变体,你换个变体也行,不写变体可以这样写 gradle build --profile

Gradle 为了加快构建速度,现在带 Task 缓存了,所以要分析构建性能还得在 AS 启动后第一次构建时进行

还可以更精准的分析构建性能 gradlew --profile --recompile-scripts --offline --rerun-tasks assembleDebug

有几个参数可配:

  • profile --> 开启性能检测
  • recompile-scripts --> 不使用缓存,直接重新编译脚本
  • offline --> 启用离线编译模式
  • return-task --> 运行所有 gradle task 并忽略所有优化

2. dependencies 命令

1. gradlew dependencies
2. gradlew app:dependencies
3. gradlew app:dependencies --configuration implementation 

dependencies 命令是用来分析依赖的,我们一般都用第3个命令,加上 module 和 implementation 限定,要不出来的信息太多,在命令行里看的太费劲,加上限定之后好多了,加上限定就是分析 module 中我们自己添加的依赖

不过说实话,信息太多,用命令行看真是蛋疼,所以用官方出了一个叫 Scan 的工具来帮助我们查看结果

3. Scan 工具

Scan 是 Google 官方推出的,用于诊断应用构建过程的性能检测工具,可以分析构建性能,能详细的看到单个 Task 的耗时,在整个构建过程中的时间点位置,也能分析依赖版本号,这点尤其有用,是我们分析依赖冲突的有力武器

在项目根目录位置下运行 gradle build --scan 即可,然后会生成 HTML 格式的分析文件的分析文件 ,中间会卡住询问 yes/on,输入 yes,有时候这个询问看不到,会一直卡在那,这时输入 yes 就行,我这就经常这样

分析文件会直接上传到 Scan 官网,命令行最后会给出远程地址

第一次跑会让你在 Scan 官网注册一下,邮件确认后就能看了,这里我们的目标是分析依赖决策(就是依赖版本号),scan 工具是按照依赖变体挨个分类的,debugCompileClassPath 就是 dedug 打包中的依赖包了

4. 分析具体依赖

分析某个具体依赖很有必要,有时候你不知道决定依赖版本的最终是哪个依赖、在哪里,用 Scan 和 命令行 都可以

我们以 rxandroid 为例,rxandroid 里依赖了 rxjava,后面我们再手动引入一个低版本的 rxjava,来看看怎么做依赖分析

implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
implementation "io.reactivex.rxjava2:rxjava:2.0.0"

1) 命令行

gradle :app:dependencyInsight --configuration debugCompileClasspath --dependency rxjava

  • 上半部分先是声明 rxjava 最终决议使用的版本是 2.2.6
  • 然后 2.2.6 这个版本的依赖来自于 rxandroid 2.1.1

2) Scan

Scan 里面看依赖分析,每一条依赖右上角都有一个小标记,单开就能看具体的分析过程了

依赖管理

在早期,Gradle 每次编译总是 迷一样,编译全靠运气 ,编译失败大部分原因都是依赖出问题了。当然现在这种情况少很多了,但是对于 依赖 我们必须搞明白,Gradle 太多的地方都有依赖的存在,不搞清楚关于依赖的部分,你发布 aar 到 Maven 都做不好,出了问题很难解决

本文解决的就是大家 一头雾水,出了问题一点头绪没有 的这种窘境。另外依赖管理是自动化构建的一大核心点,所以这块请大家务必掌握

依赖分类

Gradle 依赖分:直接依赖,项目依赖,本地 jar、aar 依赖,传递依赖,这几个总规要分清楚的,多少还是有些差别的

  • 直接依赖 --> 在项目中直接导入的依赖,就是直接依赖
implementation 'androidx.core:core-ktx:1.3.2'
  • 项目依赖 --> 这个好理解,app module 依赖 libs module,libs 就是项目依赖
  • 传递依赖 --> 依赖里面的依赖,好比 rxandroid 里面还依赖了 rxjava,rxjava 对于外层来说就是传递依赖。传递依赖就是 Gradle 做的烂的一个地方,哪个项目中传递依赖不是一层套一层的,最后套了太多层,Gradle 自己分析起来都很耗时,还容易出现问题造成编译失败,有人管这叫: 依赖地狱
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
	 --> 内部依赖了 implementation "io.reactivex.rxjava2:rxjava:x.x.x"

版本号法则

  • 1.3, 1.3.0-beta3 --> 固定版本号
  • [1.0.0, 1.3.0) --> >= 1.0.0 < 1.3.0 ,[ ) 符号都可以写
    • [ 含 =
    • ) 不含 =
  • 1.+, [1.0,) --> >= 1.0 版
  • latest.integration、latest.release --> 最新版本

因为 Gradle 没有自己的远程仓库,而是使用 Maven、jcenter、jvy 这些远程仓库,所以在添加远程依赖时,Gradle 首先按照 Maven POM 规则查找远程依赖,版本号这块大家看 Maven POM--文档 这部分就行了,写的更详细

添加依赖

注意每种资源添加依赖的区别,看着乱,但还是有规律的。Gradle 中依赖的设置都是通过 Configuration 对象完成的,可以通过代码操作,目前没看到有什么实际应用场景,应该是我段位低的缘故

1) jar 文件

module 对于依赖的 jar 包,会直接把 jar 中的代码添加到自己的项目里。举个例子,子项目 module 中有依赖 jar 文件,然后 module 打 aar 上传到 Maven,我们来看看 jar 在 aar 中如何储存的

很明显,jar 包中的类库会直接添加到构建最终产物 aar 中

正是因为这样,jar 包依赖容易出现冲突。大家想2个 aar 中依赖了相同的 jar 包,那么对于项目来说,就有2个 包名+类名 完全一样的类库,必然会冲突,解决冲突的办法下面会说

添加依赖的方式一般就是这2种了,基本上大家都是直接放到 libs 里的,一个个写太费事

implementation files('hibernate.jar', 'libs/spring.jar')
implementation fileTree(dir: 'libs', include: ['*.jar'])

对于 Gradle Android Plugin 来说,会把 jar 看成一种本地代码资源,implementation fileTree() 是声明本地 java 资源路径。其实这和依赖一个 module 项目本质上是一样的,区别是资源类型的不同,一个是 file,一个是 project

大家对比下看看

implementation project(':pickerview')
implementation files('hibernate.jar', 'libs/spring.jar')

2) so 文件

.so 文件和 .java 一样,会被 Gradle 看成一种本地代码资源,只要设置 .so 资源的路径即可

一般有2种方式:

  • 一个是按照 android 插件默认的文件路径放置 .so 文件,这样 android 插件会按照默认的路径查找,引入 so 文件
  • 一个是我们手动设置 .so 资源路径
// 设置 .so 资源路径
android{
   sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
}

需要注意的是,so 文件需要放到具体的 ABI 目录下,不能直接放 libs 目录下,一般都是这样的

x86/x86_64/armeabi-v7a/arm64-v8a 这些叫 ABI,每一种都对应一种 CPU 指令集 ,在 defaultConfig{...} 中设置使用哪些 ABI

android{
    defaultConfig {
        ndk {
            abiFilters "armeabi", "armeabi-v7a", "x86", "mips"
        }
    }
}

3) ABI 说明

Android 平台 CPU 有三大类:ARM、x86、MIPS,其中 x86、MIPS 已经被彻底淘汰了,大家集成 .so 只要考虑 ARM 平台架构就行了

ARM 平台架构主要有3种,下面说的代对应的是架构:

  • armeabi: 第5代、第6代 ARM 处理器,早期 android 手机用,A5/7/8/9 核心用的都是这个架构
  • armeabiv-v7a: 第7代、32位 ARM 处理器架构,带浮点数计算能力。A15/17 核心用的都是这个架构,一般现在也少见了,也都淘汰了
  • arm64-v8a: 第8代、64位 ARM 处理器架构,A32/35/53/57/72/73 核心用的都是这个架构,一般现在的 ARM 处理器都在这个范围内

这个大家都能看到手机 CPU 是哪家的,哪个型号的,都会宣传采用了 AXX 的 ARM 核心

一般来说现在选择集成 V7、V8 的 ABI,你要是还想兼容特别老的手机可以加上 armeabi,armeabi 的缺点就是执行速度慢,2020年开始 google paly 已经开支强制提供 64位 so 了,来自官方版的说法是很可能未来很快新的 CPU 就都是 64位并且执行支持 64 指令集的设计了

可能干说大家还是每个清晰的认识,那么结合 ARM 架构发展历程来看看吧

ARM 架构发展历程:

左侧的是架构,右侧的是处理器,也可以叫核

  • V1 架构 1985
  • V2 架构 1986
  • V3 架构 1990
  • V4 架构 1993
  • V5 架构 1998
  • V6 架构 2001
  • V7 架构 2004
  • V8 架构 2011

4) 远程依赖

Gradle 没有自己的远程仓库,用的是 Maven,Jcenter,Jvy 这些库,所以添加远程依赖,要先声明使用哪个远程仓库

每个 Gradle 脚本都要声明使用的远程仓库,所以根目录脚本才会用 allprojects{...} 给每个子项目都声明远程仓库地址

buildscript {
    ext.kotlin_version = "1.4.10"
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.0.1"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

下面这么写可能会好看一些 (○` 3′○)

dependencies {

    androidTestImplementation([
            'androidx.test.ext:junit:1.1.2',
            'androidx.test.espresso:espresso-core:3.3.0'
    ])
    
    implementation([
            'androidx.appcompat:appcompat:1.2.0',
            "io.reactivex.rxjava2:rxandroid:2.1.1"
    ])
}

或者有人在 ext{...} 里面写 implementation,然后在脚本里 each 遍历添加的,不上代码了,我觉得依赖没这么多,没必要这么写,不利于查看依赖项

5) aar 文件

Gradle 把 aar 文件一样视为远程依赖,所以同样要在 repositories{...} 里声明 aar 文件仓库地址,不过这个仓库的设置就有些麻烦、复杂了

若是 app module 依赖本地 aar,这么写就行了

android{
	...
}

// 声明本地 aar 文件地址
repositories {
	flatDir {
		dirs 'libs'
	}
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"]) 
    // 添加本地 aar 文件依赖
    implementation(name: 'easeui', ext: 'aar')

但要是 lib 这样的,给别的 module 提供依赖的 module,依赖 aar 文件的话,必须在整个依赖链条上所有的 module 都写上这个 aar 文件所在的仓库地址,一般这种情况,我们都是在根目录脚本 allprojects{...} 里添加 aar 仓库,注意这里 aar 仓库的地址淂写 aar 文件所在 module 这一层级的地址

allprojects {
    repositories {
        google()
        jcenter()
        flatDir {
            dirs '../animal/libs'
        }
    }
}

implementation(name: 'easeui', ext: 'aar')

图示如下:app 依赖 lib module --> lib module 依赖 animal module --> animal module 中有 aar 依赖 easeui.arr

或者呢你要是嫌麻烦,可以在项目根目录下建一个 libs 文件夹,所有的 aar 都放到里面,然后在跟脚本统一设置下

依赖管理的核心是什么

implementation 导入远程依赖大家熟悉吧,那么大家知道依赖导进来是怎么管理的吗?

implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
implementation "io.reactivex.rxjava2:rxjava:2.0.0"

Gradle 管理依赖是依靠2个 Classpath:compileClasspath、runtimeClasspath

  • compileClasspath --> 编译时能使用的代码、类。当一个组件参与编译时,Gradle 就会将其放在 compileClasspath 中
  • runtimeClasspath --> 运行时能使用的代码、类。当一个组件参与打包时,Gradle 就会将其放在 runtimeClasspath 中
  1. 编译时 --> 大家熟悉吗,不熟悉的这里我说下。什么是编译时,就是我们写完远程依赖,AS 把代码下来来,我们写代码时能代用到类库时、我们自己写代码时,代码还在编写阶段,只要没编译成 .class 都叫 编译时

  2. 运行时 --> 大家熟悉吗,不熟悉的这里我说下。什么是运行时,代码我们写完了,也编译成 .class 了,在机器上安装完,跑起来,这个时候叫运行时

  3. compileClasspath --> 中包含的代码、类库,是我们在编写代码时能使用到的类库。这个 path 里要是没有我们想要调的类库,即便我们把远程依赖代码下下来也没用

  4. runtimeClasspath --> 中包含的代码、类库,是 app 跑起来之后能找到的类库。即便我们把远程依赖代码下下来并写到 compileClasspath 里了,我们写代码时可以调用对应的类库,但只要没写到 runtimeClasspath 里面去,app 跑起来一样还是会找不到类库,会 crash 的

  5. implementation --> 这些只是对于依赖库不同的管理方式,核心逻辑就是决定远程拉下来的代码是放在 compileClasspath 里还是 compileClasspath 里,还是2个都放,还是只放一个,另一个不放

结合 implementation、api 理解 compileClasspath、runtimeClasspath

可能有的同学 api 不怎么熟悉,但是没关系,往后看就行

举例:A依赖B、B依赖C,A、B、C 都是项目 module

  1. 若 A implementation B,B implementation C,那么
    • 在 B 中 C 的类库可以随便用
    • 在 A 中就不能使用 C 的类库,IED 提示找不到对应的类
    • 这是因为 implementation 引入的依赖,会把 C 加入 B 的 compileClasspath 和 runtimeClasspath,会把 C 加入 A 的 runtimeClasspath
    • 因为没有加入 A 的 compileClasspath,所以在 A 项目中使用不了对应的代码,但是因为加入了 A 的 runtimeClasspath,则会把 C 的类库打包进 APK 中
  2. 若 A implementation B,B api C,那么
    • 在 B 中 C 的类库可以随便用
    • 在 A 中 C 的类库一样可以随便用
    • 这是因为 api 引入的依赖,会把 C 加入 B 的 compileClasspath 和 runtimeClasspath,同样会把 C 加入 A 的 compileClasspath 和 runtimeClasspath
    • 因为 C 即加入了 A 的 compileClasspath ,也加入了 A 的 runtimeClasspath,所以在 A 项目中既能使用 C 的类库,也会把 C 的类库打包进 APK 中
  3. 若 B compileOnly C,则 A 无法调用 C 的代码,且 C 的代码不会被打包到 APK 中
  4. 若 B runtimeOnly C,则 A、B 都无法调用 C 的代码,但 C 的代码会被打包到 APK 中

关于这点大家可以自己去试试

深入理解 compileClasspath、runtimeClasspath

compileClasspath、runtimeClasspath 这2个 path 是每个依赖级别都有的,比如上面的例子,ABC 3个都是 module 项目,每一级 module 都有自己的 compileClasspath、runtimeClasspath 来管理自己这个级别使用到的依赖,远程依赖同样如此,比如 rxandroid 里面就依赖了远程的 rxjava,对于 rxandroid 这个远程依赖来说,同样也有 compileClasspath、runtimeClasspath 这2个 path 来管理自己的依赖。最后在构建阶段,Gradle 会逐级合并 compileClasspath、runtimeClasspath,决定哪些依赖可以在代码编写时使用,哪些依赖会打包进 APK

对于 module 项目这种本地代码依赖来说比较好理解,对于远程 aar 依赖来说就有点费解了,感谢百度团队的文章做出了详细解释:Gradle 与 Android 构建入门

当使用 Maven 规范上传 aar 时,不单单会上传 aar 二进制为念,还会上传一个 pom.xml 文件,aar 中使用的远程依赖信息就记录在这个文件里,pom 是个 xml 文件,我们看一下:

很明显,我们上传 Maven 的 aar 文件中不包含我们在编写 aar 时依赖的远程依赖代码,这些我们依赖的远程依赖以 xml 文件的形式表明,上图中的 pom 文件有2个远程依赖,一个用于 complie 编译阶段,一个用于 runtime 运行时阶段,我们在 AS 中添加这个 Maven 上的 aar 后,AS 会自动解析该 aar 中的 pom 文件,去下载相关远程依赖的代码

这下大家清楚了吧,不管是项目依赖,还是远程依赖,对于子依赖的管理都是一个规则,搞清楚2个 path 的作用,我们就能清晰的知道代码是否可以 隔着项目使用了

举个例子:rxandroid 中有依赖 rxjava,我们添加 rxandroid 的依赖后,rxjava 的代码能不能用,试了下可以用,为啥呢,我去看了 rxandroid 的源码,原来 rxandroid 中是 api 的 rxjava

Gradle 的 Scope

Gradle 这部分其实就是照着 Maven 的 Scope 来的,Scope 就是 implementation、api 这些,操作的是依赖在什么时候可以用,用 path 解释就是依赖放在哪个 path 里面,是编译时可以用,还是运行时可以用

Gradle 的 Scope 基于2个维度设计:

  • 一个就是编译时还是运行时,影响依赖添加到 path 的结果,这种 Scope 常见的就是:implementation、api 这俩货了
  • 另一个就是结合 buildType 构建类型了,这点大家看自动提示就明白了

compile 已经废弃,用 implementation 代替

其他的 Scope 还有,这几个也常见:

  • compileOnly 仅编译期有效, 不会出现在最终产物中
  • runtimeOnly 仅运行期有效, 会出现在编译产物中
  • annotationProcessor 注解处理器依赖

Gradle 的 Scope 不光可以可以影响子项目资深的 compilePath、runtimePath,还可以记住依赖传递来影响上级项目的 compilePath、runtimePath,大家再结合下 implementation、api 体会下。Gradle 的 Scope 操作的都是 path,可以是项目自己的,也可以是上级项目的,区别是范围不同。然后结合 Gradle 的依赖传递,影响最终 APK 打包时都有哪些文件

POM 文件中的 Scope

上面提到过 pom.xml 文件,子项目打包 aar 上传 Maven,子项目中依赖的远程依赖不会把代码打包到最终的 aar 文件中,而是会生成一个描述 aar 使用远程依赖状况的描述文件 pom.xml,其中每个依赖都有一个 scope 属性,该属性和 implementation、api 一样,影响的也是依赖在 path 中的位置,具体下个小节会提到,这里先看看 scope 属性有哪几种类型:

此元素引用手头任务的类路径(编译和运行时,测试等),以及如何限制依赖项的可传递性

有五个作用域:

  • compile - 这是默认范围,如果未指定则使用。编译依赖项在所有类路径中均可用。此外,这些依赖项会传播到相关项目
  • provided - 这很像编译,但是表明您希望JDK或容器在运行时提供它。它仅在编译和测试类路径上可用,并且不可传递
  • runtime - 此作用域指示依赖关系不是编译所必需的,而是执行所必需的。它在运行时和测试类路径中,而不在编译类路径中
  • test - 此范围表明该依赖关系对于正常使用该应用程序不是必需的,并且仅在测试编译和执行阶段可用。它不是可传递的
  • system - 此范围类似于,provided除了必须提供显式包含它的JAR之外。该工件始终可用,并且不会在存储库中查找

子项目打包上传 aar 时 implementation、api 对 pom.xml scope 的影响

还是上面 rxandroid、rxjava 的例子,还是用上面的那种图说事

Gradle scope 和 Maven 对照,核心内容都一样,就是写法有些变化,testCompile 对应的就是 Maven 里面的 Scope

简单来说:

  • 子项目中 rxandroid 要是 implementation rxjava,那么这个 scope 就是 runtime
  • 子项目中 rxandroid 要是 api rxjava,那么这个 scope 就是 compile

假如子项目中 rxandroid implementation rxjava,然后打包 aar 上传 Maven,我们再依赖 rxandroid 这个远程依赖库,大家猜猜会怎样,会报错,提示找不到 rxjava 的类

为啥么,大家想此时的 pom.xml 文件中,rxjava 的 scope 是 runtime 的,也就是打包 APK 时代码才会生效,我们编译、写代码时是找不到 rxjava 类库的,那特定会报错的,除非我们自己在项目中再添加 rxjava 的远程依赖才行

所以 rxandroid 基于实用性上考虑,最终还是 api 的 rxjava,这样我们引入 rxandroid 的远程依赖就不会报找不到类了,要不真的很糟心,开发人员最讨厌这种麻烦事了

依赖决议

依赖决议是指 "在编译过程中, 如果存在某个依赖的多个版本, 构建系统应该选择哪个进行构建的问题"

依赖决议处理的是远程依赖版本冲突的问题,来看下典型案例:

A、B、C 都是本地子项目 module,D 是远程依赖

  • 编译时: B 用 1.0 版本的 D,C 用 1.1 版本的 D,B 和 C 之间没有冲突
  • 打包时: 只能有一个版本的代码最终打包进 APK,对于 Gradle 来说这里就有冲突了

不过 Gradle 自有其处理方法,不考虑其他强制设置版本号的手段,就说默认。默认下,Gradle 会使用最新版本的远程依赖,使用 Scan 工具分析依赖时大家能看到

系统处理的是死的,这里我碰到过坑,就是前后版本不兼容的问题。老版本有带参数的构造函数,新版本这个带参数的构造函数直接被删除了。Gradle 默认的版本决议解决不了这种问题,这只能靠大家来协调了,谁改下,另外也是提醒我们,公司里写功能组件时一定要前后版本兼容,要不很容易出那些 莫名其妙 问题来,这种问题及其难以定位,发觉,很多时候是自己给自己埋的大坑

强制依赖版本

1) isFoce

isFoce 标记会强制使用该标记版本的依赖

dependencies {
    implementation("io.reactivex.rxjava2:rxjava:2.2.6") {
        isForce = true
    }
    implementation("io.reactivex.rxjava2:rxjava:2.2.10")
}

// 依赖决议使用 2.2.6 版本

注意:

  • 同一个 module 中,对同一个依赖多次书写 isForce,只有第一个会生效,后面的都没用
  • isForce 只作用于当前模块, 不同模块中的 isForce 相互独立
  • isForce 跨模块,这些模块相互依赖,那么以 app 的 isForce 为准,因为 app 的 脚本先执行
  • isForce 的版本并不会反映到 POM 文件中, 这个必须清楚,要使用 isForce 一定要谨慎

isForce 出现的非常早,实际使用起来问题很多,因书写不当很容易造成构建失败,现在建议不要再使用了

isForce 最大的问题是这个:

force 的版本不能比 app 主模块的版本低,要不就报错。比如 app 模块依赖于 lib, 如果 app 中引入了 rxjava:2.2.10, lib 却 force 了 rxjava:2.2.6 则会发生编译错误

遇到这个问题:

  • 去掉 lib 中的 force
  • 可以在 app 中再次 force 该依赖即可,无论高低版本

2) strictly

strictly 是一种强力依赖版本约束,官方现在推荐使用这个,可以用 !! 简写

dependencies {
    implementation("io.reactivex.rxjava2:rxjava:2.2.0!!") 
    implementation("io.reactivex.rxjava2:rxjava") {
        version {
            strictly("2.2.0")
        }
    }
}

这样就会强制使用 2.2.0 版本的依赖了

3) constraints

constraints 是 strictly 新版本的替代,东西和 strictly 是一样的

dependencies {
    implementation("io.reactivex.rxjava2:rxjava:2.2.6!!")
    constraints {
        implementation("io.reactivex.rxjava2:rxjava:2.2.6!!")
    }
}

但是同样也有问题,constraints 的版本不能比其他的低,要不也会报错,下面的代码就是有问题的,会报错

dependencies {
    implementation("io.reactivex.rxjava2:rxjava:2.2.4")

	constraints {
		implementation("io.reactivex.rxjava2:rxjava:2.2.0!!")
	}
}

4) 吐槽

这几个强制版本依赖都有问题,不当设置都会报错,说实话我觉得还是按系统默认的走就好了,选最新的版本号就好了,搞得这么复杂说不好就给自己挖坑

jar 包冲突

上面说过 .jar 文件会以代码的方式添加到最终构建产物中,aar 会直接包含 .jar 中的代码。要是本地项目和远程依赖都引入了同一个 .jar 的话会冲突的,会提示有相同包名+类名冲突的资源,这里的话可以在方便的位置用 compileOnly 代替 implementation

远程依赖冲突

Gradle 在 implementation{...} 中提供了 exclude 设置,可以忽略指定的依赖,被忽略的依赖将被视为从来没有依赖过

先熟悉下 group、module、version 指的都是哪部分

implementation("io.reactivex.rxjava2:rxandroid:2.1.1")

group = io.reactivex.rxjava2
module = rxandroid
version = 2.1.1

一般我们可以这么写

 implementation('org.hibernate:hibernate:3.1') {
        exclude module: 'cglib' 
        exclude group: 'org.jmock' 
        exclude group: 'org.unwanted', module: 'iAmBuggy' 
    }

远程依赖冲突一般我们通过 exclude、force、transitive 解决冲突,其中 force 已经不推荐使用了,下面提供一些范例:

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.android.support') {
            if (!requested.name.startsWith("multidex")) {
                details.useVersion '26.1.0'
            }
        }
    }
}

依赖传递

前文我用 implementation、api 来讲解 compileClasspath、runtimeClasspath,A依赖B、B依赖C,C作为依赖会传递给A。Gradle 就是这么一层层、逐级的合并 path,才能最终知道哪些代码应该打进 APK 文件中,即便这会带来依赖冲突、性能损耗的问题,但是正是这种机制,才能使得代码齐全,不会缺少

我们 implementation 一个远程依赖,依赖传递默认是启动的。当然依赖传递也有操作余地,系统提供了 transitive 让我们自己选择是不是要把依赖传递下去

导入远程依赖,transitive 默认是 true 的,就像下面一样

implementation("io.reactivex.rxjava2:rxandroid:2.1.1"){
	transitive(true)
}

现在我们把 transitive 写成 false 看看

例:app implementation libs、libs api rxandroid,这样我们在 app 中也能够使用 rxandroid 的 API,我们给 libs api rxandroid 设置 transitive(false) 后,app 中就找不到 rxandroid 的类库了,因为依赖无法被传递了

api("io.reactivex.rxjava2:rxandroid:2.1.1"){
	transitive(false)
}

我不知道 transitive 有什么应用场景,想来应该可以用在开源库中,比如 Google 官方组件这样大家都会用的设置 transitive 就挺合适,设置 transitive 之后,依赖是不会打入 APK 中的(除非是 app module)

变体