目录
四种依赖方式的区别
主要演示 implementation、api、compileOnly、runtimeOnly 四种依赖方式的区别。
配置 | 行为 |
implementation | Gradle 会将依赖项添加到编译类路径,并将依赖项打包到构建输出。不过,其他模块只有在运行时才能使用该依赖项。 |
api | Gradle 会将依赖项添加到编译类路径和构建输出。当一个模块包含 api依赖项时,会让 Gradle 了解该模块要以传递方式将该依赖项导出到其他模块,以便这些模块在运行时和编译时都可以使用该依赖项。 |
compileOnly | Gradle 只会将依赖项添加到编译类路径(也就是说,不会将其添加到构建输出)。如果您创建 Android 模块时在编译期间需要相应依赖项,但它在运行时可有可无,此配置会很有用。 |
runtimeOnly | Gradle 只会将依赖项添加到构建输出,以便在运行时使用。也就是说,不会将其添加到编译类路径。 |
详细描述见官方 https://developer.android.com/studio/build/dependencies#dependency_configurations
上面说的编译时,运行时,通俗的说,编译时就是能不能在 studio 中不同的模块里点出来相关依赖项中的类,
运行时就是能不能在 app 运行时执行到依赖项中的类。
我们创建一个示例工程,主模块 app ,其他的5个模块 A、B、C、D、E
各模块添加测试类
模块B:
class LibraryBee {
fun keepHealth() {
println("i am library B , 勤劳致富")
}
}
模块C:
class LibraryCat {
fun doNotCome() {
println("i am library C, 爱卿平身")
}
}
模块D:
class LibraryDog {
fun happy() {
println("I am library D, 来玩儿啊")
}
}
模块E:
class LibraryElephant {
fun run() {
println(" I am library E,力拔山兮气盖世")
}
}
模块A:
class LibraryAnt {
fun hello() {
println("hello , i am A,团结就是力量")
}
}
在 A 的 gradle 文件中使用不同的方式依赖上面的四个模块:
dependencies {
implementation project(':libraryB')//代码会在运行时,但编译时不会对外暴露
api project(':libraryC')//代码会在运行时,编译时代码会对外完整暴露
compileOnly project(':libraryD')//代码不会在运行时存在,编译时对当前模块暴露,不对外暴露
runtimeOnly project(':libraryE')//代码会在运行时,编译时不存在,不对外暴露
//凡是在运行时存在的代码,在任何模块中都可以通过反射调用到,比如 A implementation B、api C,B、C 之间可以互相调用
}
然后在模块 A 的类中尝试调用四个模块中的类:
我们发现只有 runtimeOnly 方式依赖的模块 E 中的 LibraryElephant 类找不到,所以 [ runtimeOnly 依赖方式不在编译时存在 ]
然后在 A 中添加代码:
class LibraryAnt {
fun hello() {
println("hello , i am A,团结就是力量")
println("a直接调用了 模块B")
LibraryBee().keepHealth()
println("a直接调用了 模块C")
LibraryCat().doNotCome()
println("a直接调用了 模块D")
try {
LibraryDog().happy()
} catch (e: Throwable) {
println("a直接调用 模块D 出错: ${e.message}")
}
println("a通过反射在运行时调用 模块E")
try {
val classE = Class.forName("com.hdf.librarye.LibraryElephant")
val methodE = classE.getDeclaredMethod("run")
methodE.invoke(classE.getConstructor().newInstance())
} catch (e: Exception) {
println("a通过反射在运行时调用 模块E 出错:${e.message}")
}
}
}
模块app:
gradle 中添加对模块 A 的依赖:
implementation project(':libraryA')
然后在 app 中尝试调用以上5个模块中的代码:
发现只能调用到模块 A 和模块 C 中的类,A 是直接引用,C 在 A 中是 api project(':libraryC') 方式依赖,透过模块 A 继续暴露在了 app 模块中,所以 [api 依赖方式会在编译时将依赖项中的代码对外暴露]。
同时 B、D、E 中的代码无法点出来,所以 [implementation、runtimeOnly、compileOnly依赖方式不会在编译时将依赖项中的代码对外暴露]
在 App 中添加测试代码:
login.setOnClickListener {
println("主模块只依赖 模块A, 模块A implementation 模块B、api 模块C、compileOny 模块D、runtimeOnly 模块E")
println("主模块直接调用模块A的方法")
LibraryAnt().hello()
println("主模块中调用模块B的方法:通过反射调用")
try {
val classB = Class.forName("com.hdf.libraryb.LibraryBee")
val methodB = classB.getDeclaredMethod("keepHealth")
methodB.invoke(classB.getConstructor().newInstance())
} catch (e: Exception) {
println("主模块通过反射调用模块B出错,${e.message}")
}
println("主模块中调用模块D的方法:通过反射调用")
try {
val classB = Class.forName("com.hdf.libraryd.LibraryDog")
val methodB = classB.getDeclaredMethod("happy")
methodB.invoke(classB.getConstructor().newInstance())
} catch (e: Exception) {
println("主模块通过反射调用模块D出错,${e.message}")
}
println("主模块中调用模块E的方法:通过反射调用")
try {
val classB = Class.forName("com.hdf.librarye.LibraryElephant")
val methodB = classB.getDeclaredMethod("run")
methodB.invoke(classB.getConstructor().newInstance())
} catch (e: Exception) {
println("主模块通过反射调用模块D出错,${e.message}")
}
println("主模块直接调用模块C的方法")
LibraryCat().doNotCome()
}
运行日志:
1、红框是在主模块 app 中写的代码
2、篮框是在模块 A 中写的代码
3、绿框是在模块 B 中写的代码
4、红色箭头是在模块 app 和模块 A 中写的代码
日志对比着测试代码,可以很容易的验证不同依赖方式的异同点。
另外根据上面红色箭头的日志,可以很容易的看到只有 compileOnly 依赖方式不会把代码添加到运行时,其余方式均会将依赖项的代码添加到运行时。但是 compileOnly 方式依赖的代码在 studio 中可以正常的调用,只有在运行时执行到时会报错,这就是 compileOnly 方式的潜在风险,所以使用这种方式时一定要确保依赖的项目在他处依赖进运行时了。
另外还有绿框中的日志,这是在模块 B 中通过反射正常执行了 模块 C 中的代码。而 B 和 C 没有任何依赖关系,所以也可以得出一个结论,只要依赖项能被输出到运行时,就可以在项目的任何地方通过直接调用或反射的形式调用。
再来一遍总结:
dependencies {
implementation project(':libraryB')//代码会在运行时存在,但编译时不会对外暴露
api project(':libraryC')//代码会在运行时存在,编译时代码会对外完整暴露
compileOnly project(':libraryD')//代码不会在运行时存在,编译时对当前模块暴露,不对外暴露
runtimeOnly project(':libraryE')//代码会在运行时存在,编译时不存在,编译时对当前模块和对外都不暴露
//凡是在运行时存在的代码,在任何模块中都可以通过反射调用到,比如 A implementation B、api C,B、C 之间可以互相调用
}
如何确定依赖项顺序
当存在多个模块间依赖时,如何对所有的模块排序
以下是官方给的一个示例https://developer.android.com/studio/build/dependencies#dependency-order
依赖项的列出顺序指明了每个库的优先级:第一个库的优先级高于第二个,第二个库的优先级高于第三个,依此类推。在合并资源或将清单元素从库中合并到应用中时,此顺序很重要。
例如,如果您的项目声明以下内容:
依赖 LIB_A 和 LIB_B(按此顺序)
LIB_A 依赖于 LIB_C 和 LIB_D(按此顺序)
LIB_B 也依赖于 LIB_C
那么,扁平型依赖项顺序将如下所示:
LIB_A
LIB_D
LIB_B
LIB_C
这可以确保 LIB_A 和 LIB_B 都可以替换 LIB_C;并且 LIB_D 的优先级仍高于 LIB_B,因为 LIB_A(依赖前者)的优先级高于 LIB_B。
也就是说基于以上的依赖示例,模块优先级是 A>D>B>C
那这个扁平的依赖顺序是怎么来的呢?
推导的排序算法:
感觉类似深度优先的算法,app 依赖 A 和 B, A B 之间排序是
A>B,没问题。
根据深度优先算法,这时候应该先继续以 A 为起点往下寻找,这时候发现 A 依赖 C 和 D,所以:
A>C>D
C D 没有再依赖其他,到此为止,A C D 都没有再往下依赖了,所以停止。A>C>D 作为一个整体的 A 加入到 A>B 的排序中。
所以 A>B 变为:
A>C>D>B
然后开始查找 B,即以 B 为启点往下找,发现 B 依赖 C , 所以得出
B>C
C 没有再依赖其他,所以到这整个依赖关系就结束,所以 B>C 也作为一个整体的 B,加入到 A>C>D>B 中,变为:
A>C>D>B>C
很显然,这里 C 重复了,如何取舍这一大一小两个 C 呢,依据上面的两个依赖关系 A>C>D 和 B>C,C 应该同时满足小于 A 也小于 B。所以最终的结果是 A>D>B>C
简单来看,我们可以概括为一个“保小原则”,即原始排序中出现重复的依赖项时,选择小的,保留小的那一个。
以上推导方式属于个人理解,如有错误还望指正。
那要是出现循环依赖了呢?
别怕,直接报错了:
Circular dependency between the following tasks:
:libraryA:generateDebugRFile
\--- :libraryB:generateDebugRFile
\--- :libraryA:generateDebugRFile (*)
我说靓仔你说哎→→→→→→→→→→→→→→→→→→→→→→→→→→→靓仔点赞动作帅↓
↓
↓