5、依赖管理(Dependency Management)

依赖管理,说白了就是管好你项目里那些「外来户」。

我刚开始用 Gradle 那会儿,觉得不就是加几行 dependencies 嘛,有啥好学的?结果项目一复杂,依赖冲突、版本混乱、构建时间暴涨……嗯,那叫一个酸爽。后来我才明白,依赖管理的水,比你想象的要深。

5.1 implementation vs api vs compileOnly

这三个关键字,是 Gradle 3.0 之后引入的。它们决定了依赖的「可见范围」和「传递方式」。我见过不少项目还在用 compile,那都是老黄历了。

implementation

这是最常用的。它表示:这个依赖只在当前模块内部使用,不会暴露给外部。

dependencies {
    implementation 'com.squareup.okhttp3:okhttp:4.9.3'
}

好处是什么?编译更快。因为 Gradle 知道这个依赖不会泄露出去,所以它只需要重新编译当前模块,不用连累依赖方。

我的习惯:能用 implementation 就用 implementation。除非你明确需要暴露 API,否则别用 api。

api

api 会把依赖暴露给外部。也就是说,如果模块 A 依赖了模块 B,而 B 用了 api 引入了一个库,那么 A 也能直接使用这个库。

dependencies {
    api 'com.google.android.material:material:1.6.1'
}

什么时候用?比如你写了一个公共 UI 库,里面用到了 Material 组件,那调用方自然也需要这些组件。这时候用 api 就合理。

注意:滥用 api 会导致「依赖泄漏」。你改了一个底层库,结果所有上层模块都要重新编译。我在项目中遇到过这种情况,一个简单的升级,全量编译花了 20 分钟。

compileOnly

这个关键字的意思是:编译时需要,但打包时不要。说白了就是「我只借用你的接口,但运行时我自己带」。常用于注解处理器、或者某些只在编译期生效的库。

dependencies {
    compileOnly 'com.google.auto.value:auto-value-annotations:1.9'
}

我曾经在做一个插件化项目时,就用 compileOnly 引入了宿主工程的接口。这样编译能通过,但最终 APK 里不会重复打包。

关键字 编译时 运行时 是否暴露给外部
implementation
api
compileOnly

5.2 依赖传递与排除

依赖传递,是 Gradle 的默认行为。你引入一个库,它自己也有依赖,那些依赖会自动拉进来。听起来很方便对吧?但问题也出在这里。

我记得有一次,项目里同时引入了两个网络库,结果它们各自依赖了不同版本的 OkHttp。构建时直接报错:Duplicate class。嗯,这就是依赖传递带来的麻烦。

如何排除传递依赖?

有两种方式:

  1. 在依赖声明中排除
  2. 全局配置排除
// 方式一:单个依赖排除
implementation('com.example:library:1.0.0') {
    exclude group: 'com.squareup.okhttp3', module: 'okhttp'
}

// 方式二:全局排除
configurations.all {
    exclude group: 'com.squareup.okhttp3', module: 'okhttp'
}

我个人更推荐方式一,因为它更精确。全局排除容易误伤,你想想看,万一其他库也需要这个依赖呢?

避坑指南:我曾经用全局排除把整个 OkHttp 给干掉了,结果某个第三方 SDK 在运行时直接崩溃。排查了整整一天才发现是依赖缺失。从那以后,我养成了「先查依赖树,再动手排除」的习惯。

查看依赖树

Gradle 提供了查看依赖树的命令:

./gradlew :app:dependencies

输出结果会以树形结构展示所有依赖,包括传递依赖。你可以清楚地看到哪个库引入了什么版本。

5.3 动态版本与锁定

动态版本,听起来很酷对吧?用 + 号或者 latest.release 就能自动拉取最新版。但我劝你,慎用。

// 不推荐
implementation 'com.squareup.okhttp3:okhttp:4.+'

// 也不推荐
implementation 'com.squareup.okhttp3:okhttp:latest.release'

为什么?因为不可控。你今天构建没问题,明天可能就炸了。我在一个 CI 流水线上遇到过这种情况:周五晚上构建通过,周一早上构建失败,原因是某个库的补丁版本改了 API。嗯,那个周末我过得并不好。

依赖锁定

Gradle 提供了依赖锁定机制,可以固定所有依赖的版本。你只需要在 build.gradle 中启用:

dependencyLocking {
    lockAllConfigurations()
}

然后运行:

./gradlew dependencies --write-locks

这会生成一个 gradle.lockfile 文件。以后每次构建都会检查这个文件,版本不对就报错。

我的建议:对于生产项目,一定要用锁定。你可以定期手动更新锁定文件,但不要让它自动变化。这样既保证了稳定性,又不会错过安全更新。

5.4 本地依赖(aar/jar/module)

不是所有依赖都来自远程仓库。有时候你需要引入本地的 aar、jar,或者直接依赖本地模块。

本地 jar

implementation fileTree(dir: 'libs', include: ['*.jar'])

这个写法会把 libs 目录下所有 jar 都引入。我个人习惯把第三方 jar 放在 libs 目录,然后按功能分文件夹。

本地 aar

aar 的引入稍微麻烦一点。你需要先在 build.gradle 中声明仓库:

repositories {
    flatDir {
        dirs 'libs'
    }
}

dependencies {
    implementation(name: 'my-library', ext: 'aar')
}

或者你也可以把 aar 放到 libs 目录,然后用 fileTree 引入:

implementation fileTree(dir: 'libs', include: ['*.aar'])

本地 module

这是最灵活的方式。你可以在 settings.gradle 中引入本地模块:

include ':mylibrary'
project(':mylibrary').projectDir = new File('../MyLibrary')

然后在 build.gradle 中依赖:

implementation project(':mylibrary')

这样做的好处是,你可以直接修改模块代码,实时生效。我在做组件化改造时,就把所有业务模块都改成了本地 module 依赖。调试起来非常方便。

注意:本地 module 依赖会增加构建时间。因为每次修改都会触发重新编译。如果你只是引用一个稳定的库,建议用 aar 或远程依赖。

知识体系总览

下面这张图,是我对本章内容的总结。你可以把它当作一个「依赖管理速查表」。

依赖管理 关键字选择 implementation / api / compileOnly 传递与排除 exclude / 依赖树查看 版本管理 动态版本 / 依赖锁定 本地依赖 aar / jar / module 最佳实践 锁定版本 / 精确排除 依赖管理核心知识体系

依赖管理这件事,说难不难,说简单也不简单。关键在于理解每个工具的设计意图,以及它们在实际项目中的表现。嗯,希望你能从这一章里找到适合自己的方式。

公众号:蓝海资料掘金营,微信deep3321