Android 马甲包工程实践:从矩阵战略到可复制的 App Shell
拆解马甲包的技术本质、Gradle 多风味构建、配置驱动差异化与上架合规边界——让「换皮发版」从手工活变成工程体系。
在移动互联网的语境里,「马甲包」往往带着灰色联想——换皮、刷榜、规避审核。但从工程视角看,它解决的是一个正当且普遍的问题:
同一套核心能力,如何以不同品牌、渠道、合规策略,快速交付多个独立安装包?
字节的多 App 矩阵、工具类公司的品类铺量、出海团队的区域化品牌,底层逻辑都指向同一件事:把「产品表达」与「能力内核」解耦,让 Shell 层可配置、可批量、可观测。本文不谈灰色操作,只讨论一套能在团队内长期维护的 App Shell 工程体系。
若你已理解 App 矩阵化思维,可把马甲包看作矩阵化的最轻量落地形态——差异主要集中在品牌壳层,业务模块高度复用。
一、概念澄清:马甲包、矩阵 App、白标 App
| 术语 | 工程含义 | 典型差异点 |
|---|---|---|
| 马甲包(Shell App) | 共享核心代码,独立 applicationId 与上架身份 | 图标、名称、主题色、启动页、部分功能开关 |
| 矩阵 App | 共享中台与模块,面向不同场景的产品组合 | 模块装配不同、导航结构不同、用户画像不同 |
| 白标 App(White-label) | 为 B 端客户定制品牌,内核由供应商维护 | 客户 Logo、域名、支付主体、客服入口 |
三者不是互斥关系,而是差异化程度的递进:
核心 SDK / 业务模块(共享)
↓
配置与风味层(Flavor / BuildConfig)
↓
品牌壳层(资源、文案、商店素材)
↓
独立安装包(applicationId + 签名 + 商店 listing)专业团队的目标:前两层尽量不动代码,后两层用配置与构建流水线完成——而不是每出一个包就 Copy Project。
二、为什么需要马甲包:业务动机与工程代价
2.1 常见业务动机
| 动机 | 说明 | 工程侧要求 |
|---|---|---|
| 渠道与 ASO | 不同关键词、品类、地区占坑 | 独立包名、独立商店页、独立统计 |
| 风险隔离 | 单包下架不影响整体业务 | 签名、域名、支付商户分离 |
| A/B 品牌测试 | 验证不同定位的转化率 | 快速切换视觉与文案,埋点可对比 |
| 合规分区 | 不同地区隐私政策、广告 SDK 不同 | 按风味注入不同 SDK 与协议 |
| 外包协作 | 外包只改壳层,不碰核心 | 壳模块边界清晰、接口契约稳定 |
2.2 不工程化的真实代价
没有 Shell 体系时,团队往往会陷入:
- 每个包一个 Git 分支,合并冲突爆炸;
applicationId、第三方 Key、隐私链接散落在 20 处if-else;- 改一个 bug 要发 N 个包,漏发、发错风味成常态;
- 商店截图与包内品牌不一致,审核被拒后不知道改哪个仓库。
马甲包的本质不是「复制项目」,而是「同一仓库、多风味、配置驱动」。
三、架构分层:Shell 该薄,核心该厚
推荐与 矩阵化三层模型 对齐:
3.1 壳层只做什么
壳层(:app 或 :app-shell-*)职责应极度收敛:
Application子类:注入当前风味的配置;AndroidManifest合并:权限、Deep Link、applicationId;- 启动页、主 Activity、底部 Tab 等导航骨架;
res/下该包专属资源:mipmap、values/strings、themes。
反模式:在壳层写网络请求、写数据库、写复杂业务判断——这会让「换皮」变成「换脑」。
3.2 模块边界示例
:app // 风味入口,依赖下方模块
:feature-home // 可复用首页
:feature-profile
:lib-user-api
:lib-network
:lib-analytics
:lib-ad-api // 广告接口,具体 SDK 按风味打入:app 通过 productFlavors 决定:启用哪些 feature-*、注入哪套广告 Adapter、读取哪份 google-services.json。
四、Gradle 多风味:马甲包的核心杠杆
4.1 productFlavors 与 applicationId
每个马甲包在商店上的唯一身份是 applicationId(可与 namespace 分离,但需团队统一规范)。
// app/build.gradle.kts
android {
namespace = "com.example.core"
flavorDimensions += "brand"
productFlavors {
create("main") {
dimension = "brand"
applicationId = "com.example.app.main"
manifestPlaceholders["appName"] = "主品牌"
buildConfigField("String", "CHANNEL", "\"main\"")
buildConfigField("String", "PRIVACY_URL", "\"https://example.com/privacy/main\"")
}
create("lite") {
dimension = "brand"
applicationId = "com.example.app.lite"
manifestPlaceholders["appName"] = "轻量版"
buildConfigField("String", "CHANNEL", "\"lite\"")
buildConfigField("String", "PRIVACY_URL", "\"https://example.com/privacy/lite\"")
}
}
}<!-- AndroidManifest.xml -->
<application
android:label="${appName}"
... />4.2 风味资源目录
Gradle 按优先级合并资源:
app/src/main/res/ # 共享 app/src/main/res/values/strings.xml app/src/main/res/values/themes.xml app/src/main/res/ # 风味覆盖 app/src/main/res/mipmap-*/ic_launcher.png → 各风味独立图标 app/src/lite/res/values/strings.xml → lite 专属文案 app/src/main/res/values/colors.xml
实践建议:
- 共享文案放
main,风味只覆盖差异项; - 图标、启动图、空状态插图放风味目录,避免
main膨胀; - 主题色用
Theme.*+colorPrimary覆盖,不要在代码里硬编码#FF5722。
4.3 按风味依赖不同 SDK
广告、推送、统计、支付常因地区或合规要求不同。与 广告模块架构 一致:业务依赖 API 模块,风味层 runtimeOnly 具体实现。
productFlavors {
create("domestic") {
// ...
}
create("overseas") {
// ...
}
}
dependencies {
implementation(project(":lib-ad-api"))
"domesticRuntimeOnly"(project(":lib-ad-pangle"))
"overseasRuntimeOnly"(project(":lib-ad-admob"))
}4.4 buildTypes 与风味的组合
flavor × buildType 构成 Build Variant(如 liteRelease、mainDebug)。马甲包发版只打 *Release,但需保证:
| Variant | 用途 |
|---|---|
*Debug | 开发调试,可共用测试 Key |
*Release | 上架包,独立签名、混淆、资源压缩 |
*Staging(可选) | 预发环境 API,内测分发 |
signingConfigs {
create("mainRelease") { /* keystore */ }
create("liteRelease") { /* 另一套 keystore */ }
}
productFlavors {
create("main") {
signingConfig = signingConfigs.getByName("mainRelease")
}
create("lite") {
signingConfig = signingConfigs.getByName("liteRelease")
}
}要点:一包一签名为宜,便于所有权清晰与商店转移;不要把所有风味的 keystore 提交到 Git,用 CI 密钥管理注入。
五、配置驱动:减少壳层里的分支地狱
5.1 三层配置模型
| 层级 | 载体 | 典型内容 | 变更频率 |
|---|---|---|---|
| 构建期 | BuildConfig、风味 res | 渠道号、静态 URL、功能开关默认值 | 发版时 |
| 启动期 | 本地 assets/config.json | Tab 结构、模块开关、主题 token | 随包发布 |
| 运行期 | Remote Config / 自有配置中心 | 广告位、灰度、紧急下架 | 随时 |
{
"brandId": "lite",
"tabs": ["home", "discover", "mine"],
"features": {
"vip": false,
"community": true
},
"urls": {
"privacy": "https://example.com/privacy/lite",
"terms": "https://example.com/terms/lite"
}
}壳层 Application.onCreate() 读取配置,注入 DI 容器(Hilt / Koin),业务模块通过接口获取 BrandConfig,而不是 if (BuildConfig.CHANNEL == "lite")。
5.2 功能开关与模块装配
interface FeatureRegistry {
fun isEnabled(feature: Feature): Boolean
}
enum class Feature { VIP, COMMUNITY, AD_SPLASH }
// 壳层实现,数据来自 config.json + Remote Config
class BrandFeatureRegistry(
private val local: LocalBrandConfig,
private val remote: RemoteConfigRepository,
) : FeatureRegistry {
override fun isEnabled(feature: Feature): Boolean =
remote.featureOverride(feature) ?: local.features[feature] ?: false
}导航装配示例:
fun buildBottomNav(config: LocalBrandConfig): List<NavItem> =
config.tabs.mapNotNull { tabId ->
when (tabId) {
"home" -> NavItem.Home
"discover" -> if (features.isEnabled(Feature.COMMUNITY)) NavItem.Discover else null
"mine" -> NavItem.Mine
else -> null
}
}新增马甲包时,理想路径是:新增风味目录 + 一份 config + 商店素材,而不是改 MainActivity 里的 when。
六、第三方服务与合规清单
每个独立 applicationId 在第三方后台都是独立应用,必须逐项登记。
| 服务类型 | 每包需独立配置 | 常见失误 |
|---|---|---|
| Firebase / FCM | google-services.json | 错用 main 的 json 导致推送串包 |
| 应用统计 | 独立 App Key | 渠道数据混在一起 |
| 广告 SDK | 独立 App ID / 广告位 | 收益结算到错误主体 |
| 登录(微信 / Google) | 包名 + 签名 SHA 绑定 | 登录失败,仅 debug 正常 |
| 支付 | 商户号 / 商品 ID | IAP 无法到账 |
| 深度链接 | App Links 域名与 path | 唤起落到错误包 |
6.1 google-services.json 多风味
app/
src/main/google-services.json # 勿放默认错误文件
src/main/google-services.json.bak # 可选:文档说明
src/main/...
src/lite/google-services.json
src/main/...或使用 Firebase App Distribution 文档 推荐的按风味放置方式,构建前 CI 校验:当前 variant 的 json 内 package_name 必须与 applicationId 一致。
6.2 隐私与商店一致性
Google Play、国内各商店要求:包内隐私政策链接、权限说明、数据收集声明与商店 listing 一致。马甲包若共用后端,仍需:
- 每包独立隐私政策 URL(或同页带
brand参数); - 埋点上报带
applicationId/channel维度; - 广告与未成年人模式按包配置(参见广告模块的 Privacy Gate 设计)。
七、CI/CD:批量构建与发版流水线
手工点 Android Studio 打 10 个风味不可持续。推荐流水线:
7.1 构建命令示例
./gradlew :app:assembleLiteRelease :app:assembleMainRelease
./gradlew :app:bundleLiteRelease # AAB 上架 Google Play7.2 发版前自动校验(建议脚本化)
applicationId与商店后台登记一致;versionCode单调递增,各风味可共享或独立(团队需统一策略);google-services.json/ 第三方 Key 与风味匹配;- Release 包已混淆,
consumer-rules与proguard-rules已覆盖反射模块; - 品牌资源齐全:图标、启动页、strings 无
TODO; - 埋点验证:安装后首启事件
app_id正确。
7.3 版本号策略
| 策略 | 做法 | 适用 |
|---|---|---|
| 全局统一 | 所有风味同 versionCode | 内核同步发版 |
| 风味独立 | versionCode 带渠道偏移 | 各包迭代节奏不同 |
| 语义化 + 构建号 | 2.3.1 (23001) | 便于客服与崩溃平台检索 |
八、与 Presentation 架构的衔接
马甲包不应破坏 MVP / MVVM / MVI 的分层。壳层差异通过 UIState 的主题 token 与 导航图 注入,而不是在 ViewModel 里判断渠道。
data class BrandTheme(
val primary: Color,
val logoUrl: String,
val appName: String,
)
@HiltViewModel
class HomeViewModel @Inject constructor(
private val repo: HomeRepository,
val brandTheme: BrandTheme, // 壳层绑定实例
) : ViewModel() {
// 业务逻辑与 main / lite 无关
}Compose 场景可进一步用 CompositionLocalProvider(LocalBrandTheme provides …) 向下传递,页面组件保持纯净。
九、上架合规与风险边界(必读)
专业团队必须明确红线,避免工程能力被滥用:
| 行为 | 商店与法律风险 | 工程上「看起来一样」的区别 |
|---|---|---|
| 同一功能换皮重复上架、刷关键词 | 下架、开发者账号封禁 | 合法矩阵是差异化产品价值,不是纯重复 |
| 隐藏功能、审核后开关 | 严重违规 | Remote Config 应用于灰度,不用于欺诈 |
| 冒用他人商标、图标 | 知识产权诉讼 | 壳层资源需自有或授权 |
| 收集超范围隐私数据 | GDPR / 个人信息保护法 | 每包独立隐私声明与数据地图 |
Google Play「重复内容」政策要求:多个 App 需为用户提供 distinct value。马甲包工程化解决的是交付效率,不能替代产品差异化设计。
十、落地路线图
按团队成熟度分阶段推进:
Phase 1:单仓库双风味验证
- 抽取硬编码
applicationId、App 名、主题色到productFlavors; - 建立
src/<flavor>/res资源覆盖; - 双包可独立安装、独立统计。
Phase 2:配置化与模块边界
- 引入
assets/config.json+FeatureRegistry; - 业务迁入
feature-*模块,:app只做装配; - 第三方服务按风味拆分配置文件。
Phase 3:流水线与治理
- CI Matrix 构建全风味 Release;
- 发版 checklist 自动化;
- 崩溃、埋点、广告收益按
applicationId分 dashboard。
Phase 4:与矩阵化汇合
- 当风味差异从「仅品牌」扩展到「模块组合」时,演进为完整 App 矩阵架构;
- 壳层进一步变薄,新品类通过配置增删
feature依赖完成。
十一、常见反模式
| 反模式 | 后果 | 正解 |
|---|---|---|
| 每包一个工程副本 | 修 bug 漏发、技术债翻倍 | 单仓库多风味 |
| 壳层写业务逻辑 | 换皮等于重写 | 逻辑下沉 feature / lib |
全局 CHANNEL 字符串判断 | 不可测试、不可配置 | BrandConfig + DI |
| 共用一套签名打所有包 | 商店转移困难、风险连带 | 一包一 keystore |
| 忽略 Release 混淆验证 | 壳层反射、广告 SDK 仅 debug 可用 | CI 打 Release 做冒烟 |
| 无文档的新风味 | 外包无法接手 | 风味模板 + config 样例 |
结语
马甲包不是「野路子」,而是 App Shell 工程化 的俗成叫法。把差异收敛到构建系统与配置层,把共性沉入模块与中台,团队才能在合规前提下获得可复制的发版能力。
矩阵化的终点是生态;马甲包的起点是第一次把
applicationId从代码里解放出来。
当你能在一条 CI 流水线里打出第五个风味,且业务同学只改 JSON 与素材、不动 Kotlin 时,这套体系才算真正成立。