第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 版本冲突:常见场景与应对方略
版本冲突说白了就是:两个库用了同一个第三方库的不同版本,但彼此不兼容。
常见的冲突场景,我归纳了四种:
- 直接冲突:App和SDK都依赖同一库,版本不同
App: Guava 29.0 ; SDK: Guava 30.0 - 钻石依赖:SDK-A依赖Lib-v1,SDK-B依赖Lib-v2
App → SDK-A → Lib v1
App → SDK-B → Lib v2 - 传递提升:Gradle选择更高版本,但API不向后兼容
强制使用30.0,但SDK-A只兼容29.0 - 排除缺失:用
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图,展示了我平时做依赖分析时的核心框架:从依赖树出发,识别传递依赖风险,定位版本冲突,最后落地到兼容性决策。
3.5 实战避坑:我的依赖管理清单
做了这么多年SDK兼容测试,我总结了一个四步检查清单:
| 步骤 | 操作 | 产出 |
|---|---|---|
| 1 | 全量输出依赖树,对比旧版本SDK的基线 | 依赖变更清单 |
| 2 | 标记所有新增的传递依赖,逐个确认兼容性 | 传递依赖风险表 |
| 3 | 运行 dependencyInsight 确认冲突解决策略 |
冲突决策记录 |
| 4 | 在空App和真实宿主App中双重验证 | 兼容测试报告 |
一个小技巧:我习惯在CI脚本里把依赖树输出存为基线文件。每次SDK迭代,自动diff两次的差异。这样谁改了什么依赖,一清二楚。
diff old_deps.txt new_deps.txt
曾经让我熬夜的一个教训:某个SDK升级时,依赖树里多了一个
从那以后,我每次SDK版本升级,必做依赖树的diff对比。
kotlin-stdlib:1.5.0,但旧版SDK依赖的是 kotlin-stdlib:1.3.0。因为 implementation 的封闭性,构建没报错。但运行时宿主App里那些用Kotlin反射的模块全部炸了。原因是1.5.0改了某些反射API的行为。从那以后,我每次SDK版本升级,必做依赖树的diff对比。