依赖管理变化:新旧依赖声明方式

做 Gradle 版本升级,依赖声明这块儿是绕不开的坑。尤其是从 3.x 跳到 5.x 甚至 7.x,配置项改名、作用域重新划分,稍不留神就编译不过,或者运行时炸给你看。

我自己的项目在升到 Gradle 5.0 时,第一次跑了半天发现全模块报“依赖找不到”。后来查了一天,原来是 compile 换成了 implementation,传递依赖变了。从那以后,每次升级我都先对照新旧声明表,挨个替换。

今天就把这块儿彻底理清楚。你后面升级时直接拿来用就行。

一、变化总览:为什么非要改?

Gradle 团队觉得旧的 compile、provided 这些命名太模糊,容易让开发者搞不清依赖到底对谁可见。新配置明确了两个维度:

  • 对外公开的程度:api vs implementation
  • 使用场景:编译期、运行时、测试期

说白了,就是让模块间的依赖关系更“显式”,避免泄漏不必要的传递依赖,最终提升编译速度。我个人很赞成这个改动——你想想看,一个大项目上百个模块,如果每个 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 testCompile androidTestCompile 新配置 implementation / api compileOnly runtimeOnly testImplementation androidTestImplementation 注意:compile 映射到 implementation 还是 api,需要根据依赖是否对外暴露决定,不是简单的1:1替换。

七、总结:迁移实操三步走

  1. 全局搜索替换:先把 compile、provided、apk 等关键词定位出来,对照上面的表初步替换。
  2. 逐个模块编译检查:每改完一个模块就跑一次 ./gradlew :模块名:dependencies,看有没有丢失的依赖。
  3. 运行时测试:尤其是替换成 implementation 后,跑一遍全量 Instrumentation 测试,防止运行时类缺失。

我个人建议你在升级前先给项目打一个 git tag。一旦改坏了,git reset --hard 回撤,比重新搭环境快得多。