返回 Android
Android
17 分钟阅读

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-*)职责应极度收敛

  1. Application 子类:注入当前风味的配置;
  2. AndroidManifest 合并:权限、Deep Link、applicationId
  3. 启动页、主 Activity、底部 Tab 等导航骨架
  4. res/ 下该包专属资源:mipmapvalues/stringsthemes

反模式:在壳层写网络请求、写数据库、写复杂业务判断——这会让「换皮」变成「换脑」。

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 productFlavorsapplicationId

每个马甲包在商店上的唯一身份是 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(如 liteReleasemainDebug)。马甲包发版只打 *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.jsonTab 结构、模块开关、主题 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 / FCMgoogle-services.json错用 main 的 json 导致推送串包
应用统计独立 App Key渠道数据混在一起
广告 SDK独立 App ID / 广告位收益结算到错误主体
登录(微信 / Google)包名 + 签名 SHA 绑定登录失败,仅 debug 正常
支付商户号 / 商品 IDIAP 无法到账
深度链接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 Play

7.2 发版前自动校验(建议脚本化)

  1. applicationId 与商店后台登记一致;
  2. versionCode 单调递增,各风味可共享或独立(团队需统一策略);
  3. google-services.json / 第三方 Key 与风味匹配;
  4. Release 包已混淆,consumer-rulesproguard-rules 已覆盖反射模块;
  5. 品牌资源齐全:图标、启动页、strings 无 TODO
  6. 埋点验证:安装后首启事件 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 时,这套体系才算真正成立。


延伸阅读

相关文章

Android
公开
10 分钟
App矩阵化思维:让应用体系像生态一样进化
大中台技术支援与小队支撑业务的「中台化战略」。
Android
公开
17 分钟
当我们在谈MVP、MVVM、MVI的时候,我们到底在谈什么?
Presentation Layer 架构模式的演进脉络、职责边界与数据流向——从 MVC 到 MVI 的选型与实践
开发模式
公开
16 分钟
Android 广告模块架构:统一管理开屏、插屏、激励与全屏广告
从多 SDK 聚合、Activity/Fragment 生命周期到预加载与频控,拆解一套可复用的 Android 广告中台模块设计