Gradle 依赖 scope 与传递依赖
先记住哪两条线?
Gradle scope 不是只控制“本地能不能编译”,而是同时控制两条线:编译期谁能看到,以及运行/打包时谁会带上。
编译期可见:代码编译时,能不能识别这个类和方法
运行/打包带上:APK/AAB 最终运行时,里面有没有这个类、资源、so⚠️ “宿主编译时看不到”不等于“宿主包里不会带上”。这是理解
implementation最容易混的地方。
用一个三层关系理解
Android SDK 场景里通常有三层:
宿主 App
↓ 依赖
SDK AAR
↓ 依赖
三方库 libpag/lottie/fresco看 scope 时,不只看 SDK 自己能不能编译,还要看宿主 App 编译时能不能看到三方库,以及最终 App 打包时会不会带上三方库。
compile time 和 runtime 是什么?
compile time 是编译期,指 javac、kotlinc、aapt 等工具检查代码和资源时的阶段。
runtime 是运行期,指 App 已经打包并启动之后,真实执行代码、加载类、加载资源和加载 so 的阶段。
compile time:代码能不能编译过
runtime:App 跑起来后类、资源、so 能不能找到同一个依赖可能“宿主编译时不需要看到”,但“宿主运行时必须带上”。例如宿主没有直接写 PAGView,但 SDK 内部运行时会创建 PAGView。
api 的作用是什么?
api 的作用是告诉宿主:这个三方库已经是 SDK 对外能力的一部分,宿主编译时也要能看到。
dependencies {
api "group:name:version"
}如果 SDK 的公开接口里出现了三方库类型,就应该考虑 api。
class AdButton {
fun bindPagView(view: PAGView) {
}
}这里 PAGView 出现在 SDK 对外方法参数里。宿主编译 AdButton 的公开接口时,也必须知道 PAGView 是什么。
api 的结果:
SDK 编译能看到
宿主编译也能看到
宿主打包/运行也会带上
包体积会变大implementation 的作用是什么?
implementation 的作用是告诉宿主:这个三方库只是 SDK 内部实现细节,宿主编译时不需要看到,但运行/打包时需要带上。
dependencies {
implementation "group:name:version"
}比如 SDK 内部这样用:
class AdButton {
fun show() {
val view = PAGView(context)
}
}宿主只调用:
AdButton().show()宿主代码没有直接写 PAGView,所以宿主编译自己的代码时不需要看到 PAGView。
但 App 运行到 AdButton.show() 时,SDK 真的会创建 PAGView,所以最终 APK 里必须有 libpag。
implementation 的结果:
SDK 编译能看到
宿主编译默认看不到
宿主打包/运行会带上,前提是传递依赖被正常解析
包体积会变大⚠️
implementation不是“不打到宿主”。它是不暴露给宿主编译,但会作为运行时依赖传递给宿主。
compileOnly 的作用是什么?
compileOnly 的作用是:SDK 编译时借用一下这个依赖,但发布时不要求宿主自动带上。
dependencies {
compileOnly "group:name:version"
}compileOnly 的结果:
SDK 编译能看到
宿主编译看不到
宿主打包/运行也不会自动带上
包体积不会因为它变大它适合宿主一定会提供的依赖,例如宿主壳已有的基础库、系统环境提供的接口、只用于编译检查的 API 包。
⚠️ 如果 SDK 运行时真的会执行这个依赖里的类,但宿主没有自己提供,就会出现
ClassNotFoundException、NoClassDefFoundError或 XML inflate 失败。
implementation 和 compileOnly 到底差在哪?
核心差异是:implementation 会传递到运行/打包,compileOnly 不会。
scope SDK编译可见 宿主编译可见 宿主打包/运行 包体积
api 是 是 是 变大
implementation 是 否 是 变大
compileOnly 是 否 否 不变
runtimeOnly 否 否 是 变大所以这句话是错的:
implementation 和 compileOnly 都是自己本地编译用,不会打到宿主更准确的说法是:
implementation:SDK 编译用,宿主编译不可见,但宿主打包/运行会带上
compileOnly:SDK 编译用,宿主编译不可见,宿主打包/运行也不会带上runtimeOnly 是什么?
runtimeOnly 是只在运行/打包时需要,编译期不需要。
dependencies {
runtimeOnly "group:name:version"
}它适合反射加载、插件化、运行时才装配的能力。
runtimeOnly 的结果:
SDK 编译看不到
宿主编译看不到
宿主打包/运行会带上
包体积会变大Maven POM scope 怎么对应?
Gradle 发布成 Maven POM 后,依赖会映射成 Maven scope。
Gradle api -> Maven compile
Gradle implementation -> Maven runtime
Gradle compileOnly -> Maven provided
Gradle runtimeOnly -> Maven runtimecompile 表示下游编译和运行都能拿到。
runtime 表示下游编译时不一定可见,但运行/打包时要拿到。
provided 表示编译可用,但运行不携带,需要宿主或环境自己提供。
示例:SDK 内部使用 PAG
如果 SDK 内部代码或 XML 直接引用 org.libpag.PAGView,SDK 工程里写:
dependencies {
implementation deps.vendors.basic.pag
}含义是:PAG 是 SDK 内部实现依赖,宿主代码编译时不用直接看到,但宿主最终打包/运行时需要带上。
所以如果宿主正常消费 SDK 的 runtime 传递依赖,包体积会因为 libpag 变大。
但如果宿主构建系统没有 pag/libpag 的模块定义、没有版本号,或者没有消费 SDK 的 runtime 传递依赖,宿主仍然可能解析失败。
⚠️ 这类问题不能只看 SDK 里有没有写依赖,还要同时看:SDK scope、发布 POM、宿主是否解析传递依赖、宿主自己的依赖模块定义。
Android AAR 有哪些额外注意点?
AAR 包内会包含当前模块自己的 class、资源、manifest、assets、jniLibs 等内容。
但普通外部依赖不会自动被物理合并进 AAR,它们需要通过 Maven POM 或 Gradle Module Metadata 继续传递。
如果 XML 里直接写了三方 View,例如 <org.example.SomeView>,即使宿主代码没有 import 这个类,资源 inflate 时也需要运行时 classpath 能找到它。
xbuild 场景要额外看什么?
在 xbuild 这类多仓/模块化构建系统中,除了 Gradle scope 和 Maven POM,还要看宿主自己的模块定义。
如果 SDK 里存在 deps.vendors.basic.xxx,但宿主工程没有对应 xxx 模块定义或版本号,宿主就可能在配置阶段或依赖解析阶段失败。
排查清单
- 看 SDK 代码是否直接 import 三方类型。
- 看 XML 里是否直接写三方 View。
- 看三方类型是否出现在 SDK 对外 API 中。
- 看 SDK Gradle 中使用的是
api、implementation、compileOnly还是runtimeOnly。 - 看发布 POM 里依赖是
compile、runtime还是provided。 - 看宿主工程是否有对应模块定义和版本号。
- 看宿主是否消费 SDK 的传递依赖。
- 看失败发生在配置期、编译期、打包期还是运行期。