12、构建变体与风味:Android构建变体、SourceSet配置、产品风味

说到Android构建,我猜你肯定遇到过这种情况:

「测试版要连测试服务器,正式版要连生产服务器。」
「免费版功能少一些,付费版功能全开。」
「国内版用微信登录,海外版用Google登录。」

这些需求,说白了就是构建变体产品风味要解决的问题。Gradle Kotlin DSL 在这块做得特别顺手,类型安全让配置变得很舒服。

12.1 构建变体:Debug vs Release

构建变体(Build Variant)是 Android 构建的基础概念。每个变体 = 构建类型 × 产品风味。

构建类型最常见的就是 debug 和 release。我个人的习惯是,debug 包打开日志、不混淆、签名用 debug keystore;release 包则反过来。

// build.gradle.kts
android {
    buildTypes {
        debug {
            isMinifyEnabled = false
            applicationIdSuffix = ".debug"
            versionNameSuffix = "-debug"
        }
        release {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

这里有个细节:applicationIdSuffix 加上后,debug 和 release 包可以同时安装在同一台手机上。我在项目中经常用这个特性,一边测新功能,一边保留旧版本做对比。

小技巧: 如果你用 Kotlin DSL,buildTypes 块里的属性都有类型提示。比如 isMinifyEnabled 是 Boolean 类型,写错了 IDE 会直接报红。这就是类型安全的好处。

12.2 产品风味:免费版 vs 付费版

产品风味(Product Flavor)用来区分不同版本的 App。比如免费版、付费版、国内版、海外版。

android {
    flavorDimensions += "version"
    
    productFlavors {
        create("free") {
            dimension = "version"
            applicationIdSuffix = ".free"
            versionNameSuffix = "-free"
        }
        create("paid") {
            dimension = "version"
            applicationIdSuffix = ".paid"
            versionNameSuffix = "-paid"
        }
    }
}

你看,用 Kotlin DSL 的 create 函数创建风味,比 Groovy 的字符串配置清晰多了。每个风味都是一个类型安全的对象,可以配置 applicationId、versionName、minSdk 等等。

为什么会需要多个风味维度?举个例子:

  • 维度一:version(free / paid)
  • 维度二:market(china / global)

这样组合出来就是 2×2=4 个变体:freeChina、freeGlobal、paidChina、paidGlobal。

核心概念: 构建变体 = 构建类型 × 产品风味。比如 freeDebug、paidRelease 都是变体。

12.3 SourceSet 配置:代码与资源分离

SourceSet 是 Gradle 里管理源码和资源的概念。Android 插件默认有 main source set,所有风味和构建类型可以有自己的 source set。

目录结构大概是这样的:

app/
├── src/
│   ├── main/                    # 公共代码和资源
│   │   ├── java/
│   │   ├── res/
│   │   └── AndroidManifest.xml
│   ├── free/                    # free 风味专属
│   │   ├── java/
│   │   └── res/
│   ├── paid/                    # paid 风味专属
│   │   ├── java/
│   │   └── res/
│   ├── debug/                   # debug 构建类型专属
│   │   └── res/
│   └── release/                 # release 构建类型专属
│       └── res/

我记得有一次,项目需要免费版和付费版有不同的启动页。我就在 free 和 paid 目录下各放了一个 splash_screen.xml,main 目录里不放。Gradle 会自动合并,优先使用风味目录下的资源。

在 Kotlin DSL 里,你可以这样配置 source set:

android {
    sourceSets {
        getByName("main") {
            java.srcDirs("src/main/java")
            res.srcDirs("src/main/res")
        }
        getByName("free") {
            res.srcDirs("src/free/res")
        }
        getByName("paid") {
            res.srcDirs("src/paid/res")
        }
    }
}

嗯,这里要注意:getByName 是 Kotlin DSL 的写法,如果你写错了名字,编译时会直接报错。比 Groovy 的字符串配置安全很多。

12.4 构建变体与 SourceSet 的合并规则

Gradle 在构建时,会把 main、风味、构建类型的 source set 按优先级合并。优先级从高到低是:

  1. 构建类型 source set(如 debug、release)
  2. 产品风味 source set(如 free、paid)
  3. main source set

也就是说,如果同一个文件在多个 source set 里出现,高优先级的会覆盖低优先级的。但 AndroidManifest.xml 是合并的,不是覆盖。

我曾经踩过的坑: 在 free 风味里放了一个 strings.xml,里面只写了 app_name。结果 main 里的其他字符串资源全没了。后来才明白,资源文件是按目录合并的,不是按文件覆盖。所以风味目录里最好只放需要覆盖的资源,不要放完整的文件。

12.5 实战:用 Kotlin DSL 配置多风味构建

下面是一个完整的例子,展示如何用 Kotlin DSL 配置构建变体和风味:

android {
    namespace = "com.example.myapp"
    compileSdk = 34

    flavorDimensions += listOf("version", "market")
    
    productFlavors {
        create("free") {
            dimension = "version"
            applicationIdSuffix = ".free"
            versionNameSuffix = "-free"
            buildConfigField("boolean", "IS_PREMIUM", "false")
        }
        create("paid") {
            dimension = "version"
            applicationIdSuffix = ".paid"
            versionNameSuffix = "-paid"
            buildConfigField("boolean", "IS_PREMIUM", "true")
        }
        create("china") {
            dimension = "market"
            versionNameSuffix = "-cn"
        }
        create("global") {
            dimension = "market"
            versionNameSuffix = "-global"
        }
    }
    
    buildTypes {
        debug {
            isMinifyEnabled = false
            buildConfigField("String", "API_BASE_URL", "\"https://test.api.com\"")
        }
        release {
            isMinifyEnabled = true
            buildConfigField("String", "API_BASE_URL", "\"https://api.com\"")
        }
    }
}

这样配置后,你会得到 2×2×2=8 个构建变体。每个变体都可以有不同的代码、资源、配置。

我个人习惯用 buildConfigField 来注入环境变量。比如 API 地址、功能开关等。这样代码里直接引用 BuildConfig.API_BASE_URL 就行,不用写 if-else 判断。

12.6 知识体系图

下面这张图帮你理清构建变体、产品风味、SourceSet 之间的关系:

Android 构建变体与 SourceSet 关系图 构建类型 debug release 产品风味 free paid 风味维度 version market 构建变体(Build Variant) freeDebug freeRelease paidDebug paidRelease SourceSet(main + 风味 + 构建类型)

从图中可以清楚看到:构建类型和产品风味组合成构建变体,每个变体对应一组 SourceSet。Gradle 会按照优先级合并这些 SourceSet 中的代码和资源。

12.7 避坑指南

最后分享几个我踩过的坑:

  • 资源覆盖问题: 风味目录里不要放完整的资源文件,只放需要覆盖的部分。否则 main 里的资源会被整个替换掉。
  • BuildConfig 字段: 不同风味和构建类型可以定义不同的 BuildConfig 字段,但要注意类型匹配。String 类型要加双引号转义。
  • 依赖管理: 可以用 freeImplementationpaidImplementation 来为特定风味添加依赖。比如免费版用广告 SDK,付费版不用。
  • 签名配置: release 构建类型一定要配置签名信息,否则打出来的包没法发布。debug 构建类型会自动用 debug keystore。

构建变体和产品风味是 Android 多版本管理的核心。用 Kotlin DSL 配置起来,类型安全、代码清晰,比 Groovy 舒服太多了。你只要记住:变体 = 类型 × 风味,SourceSet 按优先级合并,就掌握了精髓。

我的建议: 刚开始用多风味时,先只加一个维度(比如免费/付费),等熟悉了再增加第二个维度。一口吃不成胖子,构建配置也一样。

公众号:蓝海资料掘金营,微信deep3321