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 是编译期,指 javackotlincaapt 等工具检查代码和资源时的阶段。
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 运行时真的会执行这个依赖里的类,但宿主没有自己提供,就会出现 ClassNotFoundExceptionNoClassDefFoundError 或 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 runtime

compile 表示下游编译和运行都能拿到。
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 模块定义或版本号,宿主就可能在配置阶段或依赖解析阶段失败。

排查清单

  1. 看 SDK 代码是否直接 import 三方类型。
  2. 看 XML 里是否直接写三方 View。
  3. 看三方类型是否出现在 SDK 对外 API 中。
  4. 看 SDK Gradle 中使用的是 apiimplementationcompileOnly 还是 runtimeOnly
  5. 看发布 POM 里依赖是 compileruntime 还是 provided
  6. 看宿主工程是否有对应模块定义和版本号。
  7. 看宿主是否消费 SDK 的传递依赖。
  8. 看失败发生在配置期、编译期、打包期还是运行期。

相关笔记