第3章:SDK依赖管理—依赖树、传递依赖、版本冲突

做SDK兼容性测试,有一件事如果你不搞清楚,后面全是坑——依赖管理。

我个人习惯,拿到一个新SDK,第一件事不是看API文档,而是先跑一遍依赖分析。原因很简单:你集成的是SDK,但运行的是整个依赖森林。

核心理念:SDK兼容性问题,60%以上源于依赖冲突,而非API变更。

3.1 依赖树:你的SDK到底带了什么?

每个SDK都不是孤岛。它依赖A库,A库又依赖B库和C库。我们把这个关系画出来,就是一棵依赖树。

你的App
 └── com.example:sdk-core:2.1.0
      ├── com.example:net-lib:1.3.0
      │    └── com.fasterxml.jackson:jackson-databind:2.12.0
      └── com.example:cache-lib:1.0.2
           └── com.google.guava:guava:30.0-jre

我曾经遇到一个案例:某个SDK的旧版本依赖了Jackson 2.10,新版本升级到了Jackson 2.13。表面上只是小版本变化,结果导致App里另一个模块的序列化逻辑全崩了。为什么?因为那个模块依赖的库只兼容Jackson 2.10~2.12。

日常检验单:
  • Maven/Gradle:./gradlew :app:dependencies 打出来的树必须审一遍
  • 重点关注间接依赖——那是问题的重灾区
  • 记录每个依赖的最低兼容版本

3.2 传递依赖:看不见的链条

说白了,传递依赖就是“你朋友的朋友”。你不直接引入某个库,但因为你的SDK引入了它,它就成了你的传递依赖。

// 你的build.gradle里只写了:
implementation 'com.example:payment-sdk:3.0.0'

// 但实际上你获得了:
// - okhttp:4.9.0 (直接)
// - okio:2.8.0 (传递)
// - gson:2.8.6 (传递)

这里有个陷阱,我吃了不少亏。Gradle的implementation和api关键字会导致不同的传递规则:

配置项 传递行为 兼容测试影响
implementation 不对外暴露传递依赖 减少冲突,但增加版本隐蔽性
api 对外暴露传递依赖 容易产生冲突,但版本透明
compileOnly 不参与传递 运行时可能缺失类,需单独提供
runtimeOnly 运行时引入,编译不参与 编译期不报错,运行期崩溃
我曾经在项目中踩过这个坑:SDK用 api 暴露了旧版Guava,导致宿主App里依赖新版Guava的模块运行时抛出 NoSuchMethodError。最后排查了两天,就是因为传递依赖版本不匹配。

3.3 版本冲突:常见场景与应对方略

版本冲突说白了就是:两个库用了同一个第三方库的不同版本,但彼此不兼容。

常见的冲突场景,我归纳了四种:

  1. 直接冲突:App和SDK都依赖同一库,版本不同
    App: Guava 29.0 ; SDK: Guava 30.0
  2. 钻石依赖:SDK-A依赖Lib-v1,SDK-B依赖Lib-v2
    App → SDK-A → Lib v1
    App → SDK-B → Lib v2
  3. 传递提升:Gradle选择更高版本,但API不向后兼容
    强制使用30.0,但SDK-A只兼容29.0
  4. 排除缺失:用exclude移除了某个依赖,但其他库运行时需要它

Gradle解决方案速查

// 方案1:强制指定版本
configurations.all {
    resolutionStrategy {
        force 'com.google.guava:guava:30.0-jre'
    }
}

// 方案2:排除冲突传递依赖
implementation('com.example:sdk-a:1.0.0') {
    exclude group: 'com.google.guava', module: 'guava'
}

// 方案3:使用平台BOM统一管理
implementation platform('com.example:my-bom:1.0.0')
implementation 'com.google.guava:guava'  // 版本由BOM决定
我的个人习惯:在回归测试前,先用Gradle的 dependencyInsight 确认每个冲突的决策路径。
./gradlew :app:dependencyInsight --dependency guava
重要:不要在生产环境用 transitive = false 全局关掉传递依赖。这样做会导致运行时类找不到,而且排查极其困难。我见过一个团队因此上线后崩了三个小时。

3.4 用一张图看懂SDK依赖管理体系

下面这张SVG图,展示了我平时做依赖分析时的核心框架:从依赖树出发,识别传递依赖风险,定位版本冲突,最后落地到兼容性决策。

SDK依赖管理体系 依赖树分析 gradle:dependencies 传递依赖检测 查看 indirect dependencies 版本冲突定位 resolutionStrategy 冲突类型判断 直接/钻石/提升 解决策略 force/exclude/BOM 回归验证 确认运行时行为一致 说明:整个流程从依赖树分析开始,最终落到回归验证,确保版本变更不引入兼容风险

3.5 实战避坑:我的依赖管理清单

做了这么多年SDK兼容测试,我总结了一个四步检查清单:

步骤 操作 产出
1 全量输出依赖树,对比旧版本SDK的基线 依赖变更清单
2 标记所有新增的传递依赖,逐个确认兼容性 传递依赖风险表
3 运行 dependencyInsight 确认冲突解决策略 冲突决策记录
4 在空App和真实宿主App中双重验证 兼容测试报告
一个小技巧:我习惯在CI脚本里把依赖树输出存为基线文件。每次SDK迭代,自动diff两次的差异。这样谁改了什么依赖,一清二楚。
diff old_deps.txt new_deps.txt
曾经让我熬夜的一个教训:某个SDK升级时,依赖树里多了一个 kotlin-stdlib:1.5.0,但旧版SDK依赖的是 kotlin-stdlib:1.3.0。因为 implementation 的封闭性,构建没报错。但运行时宿主App里那些用Kotlin反射的模块全部炸了。原因是1.5.0改了某些反射API的行为。
从那以后,我每次SDK版本升级,必做依赖树的diff对比。