痛点场景:为什么需要多SDK合并?
场景描述
假设你是一家物联网公司的SDK开发者,需要为不同客户提供蓝牙标签管理SDK。你的产品矩阵是这样的:
客户A(零售商超)
- 需要功能:刷图、固件升级
- 预算有限,只要基础功能
客户B(仓储物流)
- 需要功能:刷图、固件升级、巡检、定位
- 愿意为增值功能付费
客户C(智能工厂)
- 需要功能:刷图、固件升级、点灯提示、修改广播间隔、温湿度监控
- 需要深度定制
传统方案的困境
方案1:给每个客户提供完整SDK
❌ 问题:
- 客户A可以通过反编译发现隐藏功能,不付费就能使用
- 功能泄露导致定价策略失效
- 竞争对手可能通过客户A获取完整代码
方案2:为每个客户单独维护代码仓库
❌ 问题:
- 底层bug修复需要同步到N个仓库
- 代码维护成本指数级增长
- 版本管理混乱,容易出现不一致
方案3:使用多个Gradle module依赖
❌ 问题:
- 生成的AAR可能不包含依赖模块的代码
- fat-aar插件在Gradle 8+兼容性差
- 打包后体积大,包含不必要的中间产物
- IDE中代码频繁爆红,开发体验差
理想方案的需求
我们需要一个方案能够:
✅ 代码复用:底层核心代码只维护一份
✅ 功能差异化:不同客户SDK包含不同功能
✅ 打包简洁:输出单一AAR/JAR文件,集成简单
✅ 开发体验好:IDE不爆红,代码提示正常
✅ 混淆灵活:可针对不同客户配置不同的混淆规则
✅ 安全可控:客户无法通过反编译获取未授权功能
真实痛点示例
// 客户A的SDK(基础版)
public class MTTagBleManager {
public void updateImage() { ... } // ✅ 提供
public void upgradeFirmware() { ... } // ✅ 提供
// private void inspect() { ... } // ❌ 混淆/删除
// private void setLED() { ... } // ❌ 混淆/删除
}
// 客户B的SDK(增强版)
public class MTTagBleManager {
public void updateImage() { ... } // ✅ 提供
public void upgradeFirmware() { ... } // ✅ 提供
public void inspect() { ... } // ✅ 提供
public void locate() { ... } // ✅ 提供
// private void setLED() { ... } // ❌ 混淆/删除
}
传统module依赖无法实现:
- 无法针对不同客户定制ProGuard规则
- 底层module的build.gradle在打包时不生效
- 需要手动管理每个客户的功能开关
本文解决方案
通过源码合并 + settings.gradle配置的方式,实现:
项目结构:
SDK_Base(基础工具层)
↓
SDK_Core(核心功能层)
↓
SDK_Standard(客户A)→ 输出 standard.aar
SDK_Enhanced(客户B)→ 输出 enhanced.aar
SDK_Premium(客户C) → 输出 premium.aar
每个SDK壳:
- 共享底层代码(源码合并,单一维护)
- 独立混淆配置(功能差异化)
- 输出独立AAR(客户无法交叉使用)
- IDE体验完美(代码不爆红)
效果对比:
| 方案 | 代码维护 | 功能控制 | 打包质量 | IDE体验 | 安全性 |
|---|---|---|---|---|---|
| 完整SDK | 简单 | ❌ 无法控制 | ✅ 良好 | ✅ 良好 | ❌ 差 |
| 多仓库 | ❌ 复杂 | ✅ 可控 | ✅ 良好 | ✅ 良好 | ✅ 好 |
| module依赖 | ✅ 简单 | ⚠️ 有限 | ⚠️ 一般 | ❌ 差 | ⚠️ 一般 |
| 源码合并(本方案) | ✅ 简单 | ✅ 可控 | ✅ 优秀 | ✅ 优秀 | ✅ 好 |
一、核心原理
1.1 基本思路
结论先行:源码合并 + 移除module依赖 + 移除settings.gradle配置
在Android开发中,当需要将多个SDK模块打包成一个JAR/AAR文件时,最稳定可靠的方案是将依赖库作为源码目录加入,而非使用传统的Gradle module依赖方式。
1.2 为什么不用依赖方式?
传统的module依赖方式:
dependencies {
api project(':sdk-core-lib') // ❌ 不推荐
}
存在的问题:
- 生成的AAR可能不包含依赖模块的完整代码
- 需要额外处理传递依赖
- fat-aar插件在Gradle 8+版本兼容性差
- SDK壳中的代码会爆红(找不到依赖模块的类)
1.3 源码合并的工作原理
✅ 最佳方案:把依赖库当做"源代码目录"加入
这种方式不将其视为独立的Gradle module,而是直接作为源码目录,让Android Studio能正常识别和引用。
目录结构示例:
项目根目录/
├── SDK_Standard/ (主模块)
│ ├── src/main/java/
│ └── build.gradle
├── SDK_Core/ (作为源码目录)
│ └── src/main/java/
└── SDK_Base/ (作为源码目录)
└── src/main/java/
第一步:在主模块的build.gradle中配置sourceSets
android {
sourceSets {
main {
java.srcDirs += ['../SDK_Core/src/main/java']
java.srcDirs += ['../SDK_Base/src/main/java']
// 如果有资源文件
res.srcDirs += ['../SDK_Core/src/main/res']
res.srcDirs += ['../SDK_Base/src/main/res']
}
}
}
dependencies {
// ⚠️ 删除原来的module依赖
// api project(':SDK_Core')
// api project(':SDK_Base')
}
第二步:从settings.gradle中移除依赖模块
// settings.gradle
rootProject.name = 'MySDKProject'
include ':SDK_Standard' // ✅ 保留主模块
// include ':SDK_Core' // ❌ 删除
// include ':SDK_Base' // ❌ 删除
关键优势:
- ✅ Android Studio自动识别源码,代码不再爆红
- ✅ 无Gradle module依赖问题
- ✅ 效果等同于直接拷贝代码,但目录结构更清晰
- ✅ 便于多SDK壳共享底层代码
注意事项:
- 不支持Gradle的dependency graph(因为不是module)
- 这正是我们想要的"作为源码目录存在"的效果
二、实践场景
2.1 典型SDK分层结构
在实际项目中,SDK通常采用分层架构:
SDK_Standard(标准版)
├── SDK_Core(核心功能层)
│ └── SDK_Base(基础工具层)
├── 功能:刷图 + 升级
SDK_Custom_A(定制版A)
├── SDK_Core
│ └── SDK_Base
├── 功能:刷图 + 升级 + 巡检
SDK_Custom_B(定制版B)
├── SDK_Core
│ └── SDK_Base
├── 功能:刷图 + 升级 + 点灯 + 修改广播间隔
2.2 合并策略
每个SDK壳只保留一个对外接口类(如MTTagBleManager),底层代码通过源码合并方式整合:
- 源码合并:将SDK_Core和SDK_Base的代码合并到SDK壳中
- 配置统一:所有混淆、打包配置在顶层SDK壳中完成
- 功能差异化:通过不同的混淆规则控制对外开放的功能
三、核心规则(⭐ 极重要)
规则1:所有module必须以源码形式合并
正确的合并层级:
SDK_Standard(顶层AAR输出)
↓ 源码合并
SDK_Core
↓ 源码合并
SDK_Base
规则2:只有顶层build.gradle有效
使用源码合并后:
- ✅ SDK_Standard的
build.gradle→ 有效 - ❌ SDK_Core的
build.gradle→ 失效 - ❌ SDK_Base的
build.gradle→ 失效
失效的配置项包括:
dependencies(需要手动迁移到顶层)buildConfigField(需要手动生成BuildConfig)proguard-rules.pro(需要合并到顶层混淆文件)manifestPlaceholders(需要在顶层重新配置)
规则3:资源文件自动合并,但需注意冲突
资源合并规则:
- 同名资源会被顶层module覆盖
- 建议使用命名前缀避免冲突:
-
SDK_Base→base_xxxSDK_Core→core_xxxSDK_Standard→standard_xxx
规则4:避免包名和类名冲突
错误示例:
SDK_Base/com/company/utils/Utils.kt
SDK_Core/com/company/utils/Utils.kt // ❌ 冲突!
正确做法:
SDK_Base/com/company/base/utils/Utils.kt
SDK_Core/com/company/core/utils/CoreUtils.kt
SDK_Standard/com/company/sdk/utils/SdkUtils.kt
四、完整实现步骤
4.0 调试与打包的配置切换(⭐ 重要实践经验)
在实际开发中,我们发现了一个调试时代码爆红的问题及其解决方案:
问题描述:
- 使用
sourceSets方式合并源码后,IDE 中代码会爆红 - 特别是多个 SDK 模块同时引用同一个源码目录时,会出现
Duplicate content roots detected警告 - 虽然项目可以正常编译运行,但开发体验很差
解决方案:调试与打包分离配置
android {
namespace 'com.company.sdk.standard'
compileSdk 34
defaultConfig {
minSdk 21
targetSdk 34
versionCode 1
versionName "1.0.0"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
// 🔴 打包时启用 sourceSets(注释掉 dependencies 中的 api project)
// 🟢 调试时注释掉 sourceSets(启用 dependencies 中的 api project)
// sourceSets {
// main {
// java.srcDirs += ['../mi.ard.java.esl-v3-lib/src/main/java']
// java.srcDirs += ['../SDK_Core/src/main/java']
// java.srcDirs += ['../SDK_Base/src/main/java']
//
// res.srcDirs += ['../mi.ard.java.esl-v3-lib/src/main/res']
// res.srcDirs += ['../SDK_Core/src/main/res']
// res.srcDirs += ['../SDK_Base/src/main/res']
// }
// }
}
dependencies {
// 🟢 调试时启用:IDE 可以正常识别类,代码不爆红
// 🔴 打包时注释掉:避免 module 依赖打包
api project(':mi.ard.java.esl-v3-lib')
api project(':SDK_Core')
api project(':SDK_Base')
// 其他第三方依赖
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.code.gson:gson:2.10.1'
}
使用方式:
| 场景 | sourceSets | dependencies 中的 api project | 效果 |
|---|---|---|---|
| 日常开发调试 | ❌ 注释掉 | ✅ 启用 | IDE 识别正常,代码不爆红 |
| 打包发布 AAR | ✅ 启用 | ❌ 注释掉 | 源码合并,输出单一 AAR |
操作步骤:
// ========== 开发调试模式 ==========
android {
// sourceSets 全部注释
}
dependencies {
api project(':mi.ard.java.esl-v3-lib') // ✅ 启用
}
// ========== 打包发布模式 ==========
android {
sourceSets {
main {
java.srcDirs += ['../mi.ard.java.esl-v3-lib/src/main/java'] // ✅ 启用
}
}
}
dependencies {
// api project(':mi.ard.java.esl-v3-lib') // ❌ 注释掉
}
核心原理:
- 调试时:使用 Gradle module 依赖,IDE 可以通过依赖图找到所有类
- 打包时:使用 sourceSets 合并源码,确保最终 AAR 包含完整代码
⚠️ 注意事项:
- 两种配置不能同时启用,否则会导致类重复定义
- 切换配置后必须执行
Gradle Sync - 如果多个 SDK 模块共享源码,调试模式下需要在
settings.gradle中保留共享模块的声明 - 打包前务必检查配置,避免将 module 依赖打进 AAR
4.1 配置顶层build.gradle(打包模式)
android {
namespace 'com.company.sdk.standard'
compileSdk 34
defaultConfig {
minSdk 21
targetSdk 34
versionCode 1
versionName "1.0.0"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
// ⭐ 关键配置:将下层SDK作为源码目录加入
sourceSets {
main {
// 方式1:使用相对路径(推荐)
java.srcDirs += ['../SDK_Core/src/main/java']
java.srcDirs += ['../SDK_Base/src/main/java']
// 方式2:使用绝对路径
// java.srcDirs += file("${rootDir}/SDK_Core/src/main/java")
// java.srcDirs += file("${rootDir}/SDK_Base/src/main/java")
// 合并资源文件(如需要)
res.srcDirs += ['../SDK_Core/src/main/res']
res.srcDirs += ['../SDK_Base/src/main/res']
}
}
}
dependencies {
// 迁移SDK_Core的依赖
implementation 'androidx.core:core-ktx:1.12.0'
// 迁移SDK_Base的依赖
implementation 'com.google.code.gson:gson:2.10.1'
// 其他必要依赖
implementation 'androidx.appcompat:appcompat:1.6.1'
}
4.2 修改settings.gradle
配置方式取决于当前是调试模式还是打包模式:
调试模式的 settings.gradle
// settings.gradle(项目根目录)
rootProject.name = 'MySDKProject'
// ✅ 保留所有模块声明(包括共享源码模块)
include ':SDK_Standard'
include ':SDK_Enhanced'
include ':SDK_Premium'
include ':mi.ard.java.esl-v3-lib' // ✅ 调试时需要
include ':SDK_Core' // ✅ 调试时需要
include ':SDK_Base' // ✅ 调试时需要
打包模式的 settings.gradle
// settings.gradle(项目根目录)
rootProject.name = 'MySDKProject'
// ✅ 只保留顶层主模块
include ':SDK_Standard'
include ':SDK_Enhanced'
include ':SDK_Premium'
// ❌ 删除共享源码模块的声明(它们作为 sourceSets 存在)
// include ':mi.ard.java.esl-v3-lib'
// include ':SDK_Core'
// include ':SDK_Base'
为什么要这样配置:
| 配置项 | 调试模式 | 打包模式 | 原因 |
|---|---|---|---|
| 共享模块声明 | ✅ 保留 | ❌ 删除 | 调试时需要依赖图,打包时作为源码目录 |
| module 依赖 | ✅ 启用 | ❌ 注释 | 调试时让 IDE 找到类 |
| sourceSets | ❌ 注释 | ✅ 启用 | 打包时合并源码 |
⚠️ 实践建议:
由于频繁切换 settings.gradle 比较麻烦,推荐保持调试模式的配置:
// settings.gradle - 推荐配置(始终保留所有模块)
include ':SDK_Standard'
include ':mi.ard.java.esl-v3-lib'
include ':SDK_Core'
include ':SDK_Base'
然后只在 build.gradle 中切换 sourceSets 和 dependencies,这样更方便。
4.3 处理BuildConfig失效问题
问题描述: 源码合并后,原模块的BuildConfig类无法生成,导致编译错误:
import com.company.sdk.core.BuildConfig; // ❌ 找不到该类
解决方案A:修改源码引用(推荐)
将原模块中的BuildConfig引用改为顶层模块:
// 修改前
import com.company.sdk.core.BuildConfig;
// 修改后
import com.company.sdk.standard.BuildConfig;
解决方案B:手动创建BuildConfig(无需改源码)
在顶层模块中创建对应包名的BuildConfig:
// SDK_Standard/src/main/java/com/company/sdk/core/BuildConfig.java
package com.company.sdk.core;
public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "com.company.sdk.standard";
public static final String BUILD_TYPE = "debug";
public static final int VERSION_CODE = 1;
public static final String VERSION_NAME = "1.0.0";
}
4.3 合并混淆规则
将各层级的混淆规则整合到顶层proguard-rules.pro:
# SDK_Base的混淆规则
-keep class com.company.base.** { *; }
# SDK_Core的混淆规则
-keep class com.company.core.** { *; }
# SDK_Standard对外接口(不混淆)
-keep public class com.company.sdk.MTTagBleManager {
public <methods>;
}
# 内部实现类(可混淆)
-keepclassmembers class com.company.sdk.internal.** {
private <fields>;
}
4.4 最终目录结构
项目根目录/
├── settings.gradle(只包含 include ':SDK_Standard')
├── SDK_Standard/(主模块 - Gradle module)
│ ├── build.gradle(唯一有效的配置文件)
│ ├── proguard-rules.pro(合并所有混淆规则)
│ ├── src/main/
│ │ ├── java/
│ │ │ ├── com/company/sdk/(本层代码)
│ │ │ └── com/company/sdk/core/BuildConfig.java(手动创建)
│ │ └── res/(本层资源)
│ └── build/(构建输出)
│ └── outputs/aar/SDK_Standard-release.aar
│
├── SDK_Core/(纯源码目录 - 非Gradle module)
│ ├── src/main/
│ │ ├── java/(源码被SDK_Standard引用)
│ │ └── res/(资源被SDK_Standard引用)
│ └── build.gradle(已失效,可选删除)
│
└── SDK_Base/(纯源码目录 - 非Gradle module)
├── src/main/
│ ├── java/(源码被SDK_Standard引用)
│ └── res/(资源被SDK_Standard引用)
└── build.gradle(已失效,可选删除)
关键说明:
- ✅ SDK_Core和SDK_Base 不在settings.gradle中声明
- ✅ 它们通过sourceSets作为源码目录被引用
- ✅ Android Studio能正常识别代码,不会爆红
- ✅ 最终AAR只包含SDK_Standard一个模块的输出
五、注意事项与最佳实践
5.1 不适合源码合并的场景
以下情况不建议使用源码合并:
- 下层模块需要单独发布给第三方
- 模块包含Kotlin Multiplatform或C++代码
- 模块有复杂的buildConfig逻辑需要独立维护
- 需要保持严格的模块隔离
这些场景应考虑使用fat-aar方案(但需注意Gradle版本兼容性)。
5.2 依赖管理建议
dependencies {
// ✅ 使用统一版本管理
def kotlin_version = "1.9.10"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
// ✅ 明确标注依赖来源
// 来自SDK_Base
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
// 来自SDK_Core
implementation 'io.reactivex.rxjava3:rxjava:3.1.8'
}
5.3 版本发布流程
- 在顶层module更新版本号
- 执行混淆构建:
./gradlew assembleRelease - 输出文件位置:
SDK_Standard/build/outputs/aar/ - 验证AAR内容:
unzip -l SDK_Standard-release.aar
六、常见问题排查
Q1:SDK壳中的代码爆红,提示找不到类
症状: 在SDK_Standard中引用SDK_Core的类时,IDE显示红色下划线,提示"Cannot resolve symbol"
原因:
- 未在sourceSets中添加源码路径
- 或者settings.gradle中仍然声明了下层模块
解决步骤:
- 检查SDK_Standard的build.gradle是否添加了sourceSets配置:
sourceSets {
main {
java.srcDirs += ['../SDK_Core/src/main/java']
}
}
- 检查settings.gradle,确保已删除下层模块声明:
// ❌ 错误:保留了SDK_Core
include ':SDK_Standard'
include ':SDK_Core'
// ✅ 正确:只保留主模块
include ':SDK_Standard'
- 执行Gradle Sync:
File -> Sync Project with Gradle Files - 如仍未解决,尝试清理缓存:
File -> Invalidate Caches -> Invalidate and Restart
Q2:编译时提示"Duplicate class"
原因: 多个层级存在相同包名和类名
解决: 检查并重构包结构,确保每层使用不同的包名前缀
Q3:运行时找不到某些类
原因: 原模块的依赖未迁移到顶层
解决: 检查各层build.gradle的dependencies,全部迁移到顶层
Q4:资源文件丢失
原因: 未在sourceSets中添加res.srcDirs
解决:
sourceSets {
main {
res.srcDirs += ['../SDK_Core/src/main/res']
}
}
Q5:混淆后功能异常
原因: 混淆规则不完整
解决: 逐层检查原模块的混淆文件,合并所有keep规则
Q6:Gradle Sync失败
原因: 路径配置错误
解决: 检查sourceSets中的路径是否正确,建议使用相对路径:
// ✅ 推荐
java.srcDirs += ['../SDK_Core/src/main/java']
// ✅ 也可以
java.srcDirs += file("${rootDir}/SDK_Core/src/main/java")
七、总结
核心要点
- 源码目录方式:使用sourceSets将下层SDK作为源码目录加入,而非Gradle module
- 删除settings声明:从settings.gradle中移除下层模块,避免代码爆红
- 唯一配置点:只有顶层SDK的build.gradle生效
- 依赖迁移:所有下层依赖必须手动迁移到顶层
- BuildConfig处理:手动创建或修改引用路径
- 避免冲突:使用包名前缀和资源命名规范
- 混淆合并:整合所有层级的混淆规则
完整配置清单
1. 主模块build.gradle配置:
android {
sourceSets {
main {
java.srcDirs += ['../SDK_Core/src/main/java', '../SDK_Base/src/main/java']
res.srcDirs += ['../SDK_Core/src/main/res', '../SDK_Base/src/main/res']
}
}
}
dependencies {
// 所有下层依赖迁移到此处
}
2. settings.gradle配置:
include ':SDK_Standard' // 只保留主模块
3. 删除主模块dependencies中的module依赖:
// api project(':SDK_Core') // ❌ 删除
优势
- ✅ 输出单一AAR/JAR,集成简单
- ✅ 无需处理复杂的传递依赖
- ✅ Gradle 8+完全兼容
- ✅ 构建速度快,稳定性高
- ✅ Android Studio自动识别源码,代码不爆红
- ✅ 目录结构清晰,便于维护
适用范围
适合大多数多层级SDK的打包需求,特别是需要为不同客户提供差异化功能的场景。
快速排查步骤
如果遇到代码爆红问题:
- ✅ 检查sourceSets是否配置
- ✅ 检查settings.gradle是否已删除下层模块
- ✅ 执行Gradle Sync
- ✅ 清理缓存重启IDE