依赖管理变化:新旧依赖声明方式
做 Gradle 版本升级,依赖声明这块儿是绕不开的坑。尤其是从 3.x 跳到 5.x 甚至 7.x,配置项改名、作用域重新划分,稍不留神就编译不过,或者运行时炸给你看。
我自己的项目在升到 Gradle 5.0 时,第一次跑了半天发现全模块报“依赖找不到”。后来查了一天,原来是 compile 换成了 implementation,传递依赖变了。从那以后,每次升级我都先对照新旧声明表,挨个替换。
今天就把这块儿彻底理清楚。你后面升级时直接拿来用就行。
一、变化总览:为什么非要改?
Gradle 团队觉得旧的 compile、provided 这些命名太模糊,容易让开发者搞不清依赖到底对谁可见。新配置明确了两个维度:
- 对外公开的程度:
apivsimplementation - 使用场景:编译期、运行时、测试期
说白了,就是让模块间的依赖关系更“显式”,避免泄漏不必要的传递依赖,最终提升编译速度。我个人很赞成这个改动——你想想看,一个大项目上百个模块,如果每个 compile 都无脑传递,改一个底层库全家都得重新编译。
二、新旧映射表(核心对照)
下面这张表我建议你存下来,升级时候贴在代码旁边对照。
| 旧配置 | 新配置 | 作用说明 |
|---|---|---|
compile |
implementation 或 api |
implementation 只在当前模块内可见;api 对外暴露 |
provided |
compileOnly |
仅编译期可用,不参与打包,防止和运行时冲突 |
apk |
runtimeOnly |
仅运行时需要,编译期不可见(很少用) |
testCompile |
testImplementation |
测试代码的编译+运行时依赖 |
androidTestCompile |
androidTestImplementation |
Android 测试的编译+运行时依赖 |
debugCompile |
debugImplementation |
只在 debug 构建类型下生效 |
三、最关键的抉择:implementation vs api
这是升级中问得最多的问题。我拿实际场景说:
假如你的模块 A 依赖了 lib X,而且模块 A 对外暴露了 lib X 的类型(比如 A 的方法返回 X 的对象),那么你应该用 api。否则,用 implementation。
经验原则:能用 implementation 就尽量用,只有确实需要把依赖暴露给下游时才用 api。原因很简单:implementation 阻断传递,下游改依赖时不会触发上游重编译,编译速度能快 30%-50%。
我曾经接手过一个老项目,所有 compile 全变成了 api。我花了三天逐个排查,发现只有不到 20% 的场景真的需要暴露。改成 implementation 后,全量编译时间从 12 分钟降到了 7 分钟。嗯,效果直观。
四、避坑指南:我踩过的几个雷
坑1:替换 compile 时无脑全写 implementation 会怎样?
我当时遇到过,模块 B 依赖了模块 C,模块 C 中用了 implementation 导入了 Retrofit。模块 B 自己又写了 Retrofit 的注解。编译没问题,一运行就报 ClassNotFoundException。因为 Retrofit 没有传递到 B 的 classpath 里。
解决方法:如果模块 C 里的 Retrofit 类型被模块 B 的代码直接用到,那就改成 api。你可以用 Android Studio 的 "Find Usages" 检查这个依赖在别的模块里是否被引用。
坑2:compileOnly 和 annotationProcessor 别搞混
以前 provided 用来放 annotation processor 依赖,比如 Dagger、ButterKnife。现在 provided 已经没用了,应该用 annotationProcessor。而 compileOnly 只用来放那些“编译时需要但运行时由容器提供”的库,比如 javax.inject。
小技巧:如果你不确定某个旧依赖该映射成什么,可以用 Gradle 的 dependencies 任务查看模块的实际依赖树,再对照上面的表。
五、新特性:依赖约束和强制覆盖
Gradle 6.0+ 还引入了 dependency constraints 和 forced dependencies,让依赖管理更精细。我觉得和旧声明方式迁移有关联,顺便提一下。
比如你想强制所有模块都用 1.2.3 版本的某个库,而不是传递来的低版本:
dependencies {
implementation("org.example:some-lib:1.2.3") {
// 强制使用这个版本,覆盖所有传递依赖中的不同版本
force = true
}
}
或者用约束统一管理版本:
dependencies {
constraints {
implementation('org.example:some-lib:1.2.3') {
because '修复了安全漏洞'
}
}
}
这些功能在旧版本里要么没有,要么靠 configurations.all { resolutionStrategy { force ... } } 实现,现在更直观了。
六、知识图谱:一句话总结整个迁移路径
下面这张图帮你理清脉络——旧声明向左走不通,向右是新世界:
七、总结:迁移实操三步走
- 全局搜索替换:先把
compile、provided、apk等关键词定位出来,对照上面的表初步替换。 - 逐个模块编译检查:每改完一个模块就跑一次
./gradlew :模块名:dependencies,看有没有丢失的依赖。 - 运行时测试:尤其是替换成
implementation后,跑一遍全量 Instrumentation 测试,防止运行时类缺失。
我个人建议你在升级前先给项目打一个 git tag。一旦改坏了,git reset --hard 回撤,比重新搭环境快得多。