4. 项目结构适配要点
项目结构适配,说白了就是让你的 SDK 能舒服地躺在别人的项目里。别小看这事,我见过太多 SDK 因为 Manifest 合并冲突、模块依赖混乱,把宿主项目搞崩的案例。今天咱们就从三个最头疼的地方入手:Manifest 配置、多模块项目、构建变体。
核心原则:用最小的侵入性,完成最大的适配性。你的 SDK 像客人,不是主人。
4.1 AndroidManifest.xml 配置最佳实践
这个文件是 Android 系统的入口清单。你声明了什么,系统就给你什么权限和组件。但 SDK 的 Manifest 和 App 的 Manifest 会合并,冲突就来了。
4.1.1 合并冲突的根源
Gradle 在构建时,会把所有模块的 AndroidManifest.xml 合并成一个。如果两个 Manifest 定义了同一个属性,比如 android:icon,就会报错。
看我常用的解决三板斧:
- 使用 tools:replace – 告诉合并器「用我的覆盖宿主」
- 使用 tools:node="remove" – 直接删掉宿主里你不想要的声明
- 使用 tools:node="merge" – 强制合并列表属性
举个例子。你的 SDK 需要一个 minSdkVersion 为 21,但宿主项目是 19。按下面写:
<!-- SDK 的 AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-sdk
android:minSdkVersion="21"
tools:overrideLibrary="com.your.sdk" />
<activity
android:name=".SdkActivity"
android:theme="@style/SdkTheme"
tools:replace="android:theme" />
</manifest>
tools:overrideLibrary 的意思是「我知道宿主最低版本低,但我这个库必须 21,你忍一下」。而 tools:replace 则是「宿主如果也配了 theme,拿我的替换」。我在字节做 SDK 适配时,因为没加 replace,导致宿主主题盖了我的,整个页面渲染都是乱的。嗯,从那以后我就养成了「能写 replace 就别偷懒」的习惯。
注意:不要滥用 tools:node="remove"。你删了宿主的 activity,万一对方 Debug 要用,你就等着被提 bug 吧。尽量用 replace 或者 merge。
4.1.2 权限声明要克制
很多 SDK 上来就声明 READ_PHONE_STATE、ACCESS_FINE_LOCATION。讲真,用户打开应用发现一个计算器要定位,直接卸载。你的 SDK 应该按需声明,最好做成可选权限。
| 场景 | 权限 | 建议做法 |
|---|---|---|
| 基础广告 SDK | INTERNET, ACCESS_NETWORK_STATE | 直接声明,无风险 |
| 需要获取设备标识 | READ_PHONE_STATE | 做成可选权限,动态申请 |
| 位置相关服务 | ACCESS_FINE_LOCATION | 用 tools:node="remove" 去掉默认声明,让宿主决定 |
| 存储读写 | READ/WRITE_EXTERNAL_STORAGE | Android 10+ 已废弃,建议用 SAF 或 MediaStore |
我个人习惯是:除了网络和 WIFI 状态,其他一律不加默认声明。宿主需要时,他们自己加,或者我在 SDK 接入文档里说明「如果需要 XX 功能,请在宿主 Manifest 中添加 XX 权限」。
4.2 多模块项目适配
现在的大型 App 基本都是多模块架构:基础库模块、业务模块、动态模块。你的 SDK 要同时兼容 单模块项目 和 多模块项目。
4.2.1 模块依赖关系设计
我推荐这样组织 SDK 的模块:
sdk-core // 核心代码,无 UI 依赖
sdk-ui // UI 组件,依赖 sdk-core
sdk-third-party // 第三方集成辅助,依赖 sdk-core
sdk-annotations // 注解定义,无任何依赖
为什么这么分?你想想看,如果宿主项目只用你的核心算法,他引入 sdk-core 就行;如果要 UI,再引入 sdk-ui。这样依赖粒度小,宿主可以按需选择。
小技巧:用 api 和 implementation 区分依赖关系。api 会透传到宿主,implementation 只在模块内可见。我一般只在 sdk-annotations 上用 api,其他全部 implementation 避免污染宿主。
4.2.2 多模块下的 Manifest 合并策略
多模块意味着多个 AndroidManifest.xml。每个模块的声明都会参与合并。我曾经遇到一个问题:UI 模块声明了一个 Activity,但 core 模块也声明了一个同名的 provider,两个模块的 authorities 一样,合并时直接报错。
解决方法是:使用 BuildConfig 动态生成 authorities。
<!-- UI 模块的 AndroidManifest.xml -->
<provider
android:name=".SdkFileProvider"
android:authorities="${applicationId}.sdk.fileprovider"
android:exported="false" />
在模块的 build.gradle 里:
android {
defaultConfig {
manifestPlaceholders = [
sdkAuthority: "${applicationId}.sdk.fileprovider"
]
}
}
这样每个宿主的 applicationId 不同,provider 的 authority 就不会冲突。记住,多模块下 authorities、permission、content provider 的 URI 最容易冲突,最怕死磕静态字符串。
4.3 构建变体(Build Variants)的使用
构建变体 = Build Type + Product Flavor。这是 Gradle 体系里最有用的特性,没有之一。
4.3.1 场景:开发版 vs 正式版
你的 SDK 在调试时需要日志输出,但上线后必须关闭。以前有人用 BuildConfig.DEBUG,但这个变量在 SDK 模块里不一定可靠。我用的是构建变体:
android {
buildTypes {
debug {
buildConfigField "boolean", "LOG_ENABLE", "true"
}
release {
buildConfigField "boolean", "LOG_ENABLE", "false"
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
在代码中:
if (BuildConfig.LOG_ENABLE) {
Log.d("SdkTag", "request params: $params")
}
注意:release 变体下,BuildConfig.LOG_ENABLE 会被编译为 false,加上 minifyEnabled 后,Log 代码会被 ProGuard 直接删掉——性能也兼顾了。
4.3.2 场景:不同渠道的 API 地址
你的 SDK 可能对接不同的后端环境:测试环境、预发布环境、生产环境。用 Product Flavor 解决:
android {
flavorDimensions "server"
productFlavors {
dev {
dimension "server"
buildConfigField "String", "BASE_URL", "\"https://dev-api.yoursdk.com/\""
}
staging {
dimension "server"
buildConfigField "String", "BASE_URL", "\"https://staging-api.yoursdk.com/\""
}
prod {
dimension "server"
buildConfigField "String", "BASE_URL", "\"https://api.yoursdk.com/\""
}
}
}
然后宿主 Gradle 里选中 devDebug 或 prodRelease。我个人习惯是把 所有与环境相关的配置都丢进 buildConfigField,不写在代码里。这样换环境只要改 Gradle,不用发版。
最佳实践:构建变体也可以控制 Manifest 的声明。比如调试变体下注册一个 Stetho 的 Activity,生产变体下不注册。用 sourceSets 的 manifest.srcFile 或者 manifestPlaceholders 都能实现。
4.3.3 避坑:变体矩阵爆炸
如果你定义了 2 个 flavor dimension(比如 server + market),每个维度 3 个值,那么构建变体就是 3 × 3 × 2(两个 build type)= 18 个。我之前在阿里做过一个 SDK,就是被变体矩阵拖慢了一半的 CI 构建时间。
解决方案:只在 SDK 层使用一个 dimension。或者用 filter 忽略不需要的组合。
android {
variantFilter { variant ->
if (variant.flavors*.name.contains("dev") && variant.buildType.name == "release") {
setIgnore(true)
}
}
}
这样 devRelease 就不会生成了,节省 Gradle 解析时间。
这张图是你今天的记忆地图。Manifest 在最顶层,三个分支分别是合并冲突、多模块、构建变体。无论你接入什么样的宿主项目,把这三点理清了,SDK 适配就不会出大乱子。