#.Gradle依赖配置
##.Gradle依赖管理与两个重要的classpath相关,每个Module都有:
1.编译时路径:compileClasspath
编译时能使用的代码,当一个类参与编译时,Gradle会将其放在compileClasspath中;编译过程会将源代码编译为.class文件。
2.运行时路径:runtimeClasspath
运行时使用的代码,当一个类参与打包时,Gradle就会将其放在runtimeClasspath中;运行时会将编译好的.class文件在JVM上运行。
各种不同依赖方式的关键区别就是:依赖树上的直接依赖包、间接依赖包的代码,是否允许加入编译时路径、运行时路径,或者是二者都允许加入。
Gradle3.0以后常用的关键字有implementation、api、compileOnly
1.implementation
与Gradle2.0的compile对应,会将直接依赖包同时添加到编译时路径、运行时路径,依赖包会被打入输出包中(aar或apk)。 差别在于对间接依赖包的处理上,使用implementation时直接依赖包不会向上层传递自己内部的依赖关系。
举例,A依赖B,B依赖C。对于A而言:B会同时加入A的编译时路径、运行时路径;但C不会加入A的编译时路径,只会加入A的运行时路径。因此,A在编译时只能访问B对外暴露的类和接口,不能访问C对外暴露的类和接口。因为B、C加入A的的运行时路径,所以对A打包时,B、C会打到A的输出包中。因为A不能直接访问C中的代码,所以修改C中代码时,只会影响直接依赖它的B,不会影响到更上层。所以重新对A打包时,只需要对C、B重新编译,不需要对A重新编译。
当有很多Module相互依赖,全部使用implementation时,修改一个Module,只有直接依赖它的Module受影响,绝大部分Module不需要重新编译,会显著提升构建时间。 一般建议配置依赖关系时尽可能使用implementation,而非api。
2.api
与Gradle2.0的compile对应,功能完全一样,会将直接依赖包和间接依赖包同时添加到编译时路径、运行时路径,并且会将依赖包依赖包会被打入输出包中(aar或apk)。 与 Gradle2.0的compile相同 , 编译时直接依赖包会向上层传递自己内部的依赖关系。
举例,A依赖B,B依赖C。对于A而言:B、C会同时加入A的编译时路径、运行时路径。因此,A在编译时既能访问B对外暴露的类和接口,也能访问C对外暴露的类和接口。因为B、C加入A的的运行时路径,所以对A打包时,B、C会打到A的输出包中。A能直接访问C中的代码,在编写代码调用时是方便了一些,但代价是修改C中对外暴露的代码时,不仅B受影响,A也受影响。所以重新对A打包时,C、B、A都需要重新编译。
当有很多Module相互依赖,全部使用api时,修改一个Module对外暴露的代码时,所有直接依赖它和间接依赖它的Module都受影响。如果修改的是依赖树较低层位置的Module,可能绝大部分Module后继都需要重新编译,构建时间会大大增加。 一般建议配置依赖关系时尽可能使用implementation,而非api。
3.compileOnly
与Gradle2.0的provided对应,依赖包只添加到编译时路径,不会添加到运行时路径,因此编译时可用,但不会打包到输出包中(aar或apk)。这可以减少输出包的体积,在只有编译时需要,运行时可选的情况下,很有用。例如可通过网络动态下载所需依赖包时,依赖包事先可不必打包到输出包里,当判断需要用到相关模块时,可以再下载。
4.runtimeOnly
与apk对应,依赖包不添加到编译时路径,只会添加到运行时路径,因此编译时不可用,但最终会打包到输出包中(aar或apk)。(没用过)
Gradle新老版本关键字对应一览表:
Gradle3.x+版本配置 | 已弃用配置(2.0及以前) |
api | compile |
implement | compile |
compileOnly | provided |
runtimeOnly | apk |
testImplementation | testCompile |
androidTestImplementation | androidTestCompile |
debugImplementation | debugCompile |
releaseImplementation | releaseCompile |
#.Gradle依赖冲突的出现与解决
1.依赖冲突出现的原因:
Android项目中有多个有相互依赖关系的Module,每个Module都依赖多个库(既可以是第三方库,也可以是Module打包成的aar等)。
Gradle会按照配置的依赖关系按照树形结构来做依赖解析,当发现依赖树中出现了同一个库的不同版本(意味着同一个库的不同实现代码),就会出现依赖冲突。
(默认情况下Gradle会尝试帮我们解决依赖冲突,解决的方式是使用最新的版本;这里的最新不是判断版本号大小,应该是根据发布时间来决定!没细究过其具体解决依赖冲突的方式。)
例:考虑如下场景:A 模块依赖 B1 和 C, D 模块依赖了 B2,其中 B1 和 B1 是同一个Module B的两个不同版本;同时,工程中我们同时依赖了 A 和 D。这种情况下,会存在针对Module B的依赖冲突!
2.依赖冲突的解决
2.1最好的方式,当然是依赖库不出现版本冲突。
可以自己写一个gradle脚本,例如上文中的utils.gralde,统一管理项目中用到的所有库的版本。
所有用到这些库的地方,统一都使用脚本中定义好的版本。
2.2通过transitive、exclude等关键字手动配置,解决冲突
1)transitive关键字:
对某个依赖库设置transitive关键字后,Gradle在解析依赖树时不会对该库内部的依赖关系做解析。
2)exclude关键字:
exclude关键字可以解除对依赖库内部的指定部分库的依赖解析。
3)force关键字:
force关键字可以强制设置指定Module依赖某个库。
示例,针对上面的依赖冲突示例,避免B1与B2的冲突:
(1) 方案一:针对 A 或 D 配置 transitive。
这里针对A配置,不解析A模块的传递依赖,因此当前Module的依赖关系树中不再包含 B1 和 C,这里需要手动添加依赖 C
dependencies {
implementation A {
transitive = false
}
implementation C
implementation D {
//transitive = false
}
}
(2) 方案二:针对 A 或 D 配置 exclude规则,此处针对A配置,依赖关系树中不再包含B1
dependencies {
implementation A {
exclude B1
}
implementation D {
//exclude B2
}
}
(3) 方案三:使用force强制依赖某个版本,如强制依赖 B1 或者 B2
以下是在顶层build.gradle中配置,强制所有module依赖B1
configurations.all {
resolutionStrategy {
force B1
// force B2
}
}
##.个人推荐的依赖配置方式
可以自己写一个gradle脚本,例如上文中的utils.gralde,统一管理项目中用到的所有第三方库的版本。
所有用到第三方库的地方,统一都使用脚本中定义好的版本。
至于自己写的Module,使用implementation方式,只对它直接用到的Module设置依赖关系(不要设置transitive=false,否则要把这些直接依赖包内部的包也统统设置到当前Module)。
这些可以尽可能少的配置依赖关系,而且只要直接依赖包的对外类、接口不变,无论依赖树上子孙节点内部如何修改,都不用重新编译当前包。修改代码时,需要重新编译的包较少,提高编译效率。