Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

配置编译变体

本页以“配置您的编译操作“概览为基础,向您介绍如何配置编译变体以根据单个项目编译出不同版本的应用,以及如何正确管理您的依赖项和签名配置。

每种编译变体都代表您可以编译的一个不同的应用版本。例如,您可能希望为应用编译两个版本,一个是内容有限的免费版本,另一个是包含更多内容的付费版本。您还可以根据 API 级别或其他设备变量,为应用编译以不同设备为目标的不同版本。但是,如果您想根据设备 ABI 或屏幕密度编译不同的版本,则应编译多个 APK

编译变体是 Gradle 使用一组特定规则将在版本类型和产品特性中配置的设置、代码和资源结合在一起的结果。虽然您无法直接配置编译变体,但可以配置组成它们的版本类型和产品特性。

例如,“demo”产品特性可以指定不同的功能和设备要求(如自定义源代码、资源和最低 API 级别),而“debug”版本类型则会应用不同的编译和打包设置(如调试选项和签名密钥)。生成的编译变体是应用的“demoDebug”版本,并且包括“demo”产品特性、“debug”版本类型和 main/ 源集中包含的配置和资源组合。

配置版本类型

您可以在 android 块内的模块级 build.gradle 文件中创建和配置版本类型。当您创建新模块时,Android Studio 会自动为您创建“debug”版本类型和“release”版本类型。虽然“debug”版本类型没有显示在编译配置文件中,但 Android Studio 会使用 debuggable true 配置它。这样,您就可以在安全的 Android 设备上调试应用,并使用常规调试密钥库配置 APK 签名。

如果要添加或更改某些设置,则可以将“debug”版本类型添加到您的配置中。以下示例为调试版本类型指定了 applicationIdSuffix,并配置了一个使用“debug”版本类型的设置进行初始化的“staging”版本类型。

    android {
        defaultConfig {
            manifestPlaceholders = [hostName:"www.example.com"]
            ...
        }
        buildTypes {
            release {
                minifyEnabled true
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            }

            debug {
                applicationIdSuffix ".debug"
                debuggable true
            }

            /**
             * The `initWith` property allows you to copy configurations from other build types,
             * then configure only the settings you want to change. This one copies the debug build
             * type, and then changes the manifest placeholder and application ID.
             */
            staging {
                initWith debug
                manifestPlaceholders = [hostName:"internal.example.com"]
                applicationIdSuffix ".debugStaging"
            }
        }
    }
    

注意:当您更改编译配置文件时,Android Studio 会要求您将项目与新配置同步。要同步您的项目,您可以点击通知栏(在您做出更改后,系统会立即显示此栏)中的 Sync Now,或点击工具栏中的 Sync Project 图标 。如果 Android Studio 发现您的配置有任何错误,则会显示 Messages 窗口来说明该问题。

要详细了解您可以通过版本类型配置的所有属性,请阅读版本类型 DSL 参考

配置产品特性

创建产品特性与创建版本类型类似:将其添加到编译配置中的 productFlavors 代码块并添加所需的设置。产品特性支持与 defaultConfig 相同的属性,这是因为 defaultConfig 实际上属于 ProductFlavor 类。这意味着,您可以在 defaultConfig 代码块中为所有类型提供基本配置,并且每个类型都可以更改其中任何默认值,例如 applicationId。要详细了解应用 ID,请阅读设置应用 ID

注意:您仍然需要在 main/ 清单文件中使用 package 属性指定软件包名称。此外,您还必须在源代码中使用该软件包名称来引用 R 类,或解决任何相关活动或服务注册。这样,您便可以使用 applicationId 为每个产品特性提供唯一的 ID 用于打包和分发,而不必更改您的源代码。

所有类型都必须属于一个指定的类型维度,即一个产品特性组。即使您打算只使用一个维度,也必须将类型分配到类型维度,否则您会收到以下编译错误:

      Error:All flavors must now belong to a named flavor dimension.
      The flavor 'flavor_name' is not assigned to a flavor dimension.
    

以下代码示例创建了一个名为“version”的类型维度,并添加了“demo”和“full”产品特性。这些类型提供了它们自己的 applicationIdSuffixversionNameSuffix

    android {
        ...
        defaultConfig {...}
        buildTypes {
            debug{...}
            release{...}
        }
        // Specifies one flavor dimension.
        flavorDimensions "version"
        productFlavors {
            demo {
                // Assigns this product flavor to the "version" flavor dimension.
                // This property is optional if you are using only one dimension.
                dimension "version"
                applicationIdSuffix ".demo"
                versionNameSuffix "-demo"
            }
            full {
                dimension "version"
                applicationIdSuffix ".full"
                versionNameSuffix "-full"
            }
        }
    }
    

注意:要使用 Google Play 中的多 APK 支持分发您的应用,请将同一 applicationId 值分配给所有变体,并为每个变体分配不同的 versionCode。要在 Google Play 中将应用的不同变体作为单独的应用分发,您需要为每个变体分配一个不同的 applicationId

创建并配置产品特性后,点击通知栏中的 Sync Now。同步完成后,Gradle 会根据您的版本类型和产品特性自动创建编译变体,并根据 <product-flavor><Build-Type> 对其进行命名。例如,如果您创建了“demo”和“full”产品特性,并保留默认的“debug”和“release”版本类型,则 Gradle 会创建以下编译变体:

  • demoDebug
  • demoRelease
  • fullDebug
  • fullRelease

您可以将编译变体更改为您要编译并运行的任意变体,只需依次转到 Build > Select Build Variant,然后从下拉菜单中选择一种变体即可。不过,要开始使用自己的功能和资源自定义每个编译变体,您需要知道如何创建和管理源集。 请参阅创建和管理源集

将多个产品特性与类型维度结合使用

在某些情况下,您可能需要合并多个产品特性的配置。例如,您可能需要为基于 API 级别的“full”和“demo”产品特性创建不同的配置。为此,您可以使用 Android Plugin for Gradle 创建多组产品特性作为类型维度。在编译应用时,Gradle 会结合使用您定义的每个类型维度的产品特性配置以及版本类型配置,以创建最终的编译变体。Gradle 不会将属于同一类型维度的产品特性组合在一起。

提示:要根据 ABI 和屏幕密度创建不同版本的应用,您应该编译多个 APK,而不要使用产品特性。

以下代码示例使用 flavorDimensions 属性来创建“mode”类型维度和“api”类型维度,前者用于将“full”和“demo”产品特性进行分组,后者用于根据 API 级别对产品特性配置进行分组:

    android {
      ...
      buildTypes {
        debug {...}
        release {...}
      }

      // Specifies the flavor dimensions you want to use. The order in which you
      // list each dimension determines its priority, from highest to lowest,
      // when Gradle merges variant sources and configurations. You must assign
      // each product flavor you configure to one of the flavor dimensions.
      flavorDimensions "api", "mode"

      productFlavors {
        demo {
          // Assigns this product flavor to the "mode" flavor dimension.
          dimension "mode"
          ...
        }

        full {
          dimension "mode"
          ...
        }

        // Configurations in the "api" product flavors override those in "mode"
        // flavors and the defaultConfig block. Gradle determines the priority
        // between flavor dimensions based on the order in which they appear next
        // to the flavorDimensions property above--the first dimension has a higher
        // priority than the second, and so on.
        minApi24 {
          dimension "api"
          minSdkVersion 24
          // To ensure the target device receives the version of the app with
          // the highest compatible API level, assign version codes in increasing
          // value with API level. To learn more about assigning version codes to
          // support app updates and uploading to Google Play, read Multiple APK Support
          versionCode 30000 + android.defaultConfig.versionCode
          versionNameSuffix "-minApi24"
          ...
        }

        minApi23 {
          dimension "api"
          minSdkVersion 23
          versionCode 20000  + android.defaultConfig.versionCode
          versionNameSuffix "-minApi23"
          ...
        }

        minApi21 {
          dimension "api"
          minSdkVersion 21
          versionCode 10000  + android.defaultConfig.versionCode
          versionNameSuffix "-minApi21"
          ...
        }
      }
    }
    ...
    

Gradle 创建的编译变体数量等于每个类型维度中的类型数与您配置的版本类型数量的乘积。当 Gradle 为每个编译变体或相应的 APK 命名时,属于较高优先级类型维度的产品特性会先显示,然后是属于较低优先级维度的产品特性,然后是版本类型。以上面的编译配置为例,Gradle 使用以下命名方案创建了总共 12 个编译变体:

编译变体:[minApi24, minApi23, minApi21][Demo, Full][Debug, Release]
对应的 APK:app-[minApi24, minApi23, minApi21]-[demo, full]-[debug, release].apk
例如,
编译变体:minApi24DemoDebug
对应的 APK:app-minApi24-demo-debug.apk

除了可以为各个产品特性和编译变体创建源集目录外,您还可以为每个产品特性组合创建源集目录。例如,您可以创建 Java 源文件并将其添加到 src/demoMinApi24/java/ 目录中,这样 Gradle 就只会在编译同时对应这两种产品特性的变体时才使用这些源文件。您为产品特性组合创建的源集的优先级高于属于各个产品特性的源集。要详细了解源集以及 Gradle 如何合并资源,请阅读关于如何创建源集的部分。

过滤变体

Gradle 会为您配置的产品特性和版本类型的每种可能组合创建编译变体。但是,某些编译变体可能并不是您需要的,或者在项目上下文中没有意义。您可以通过在模块级 build.gradle 文件中创建变体过滤器来移除某些编译变体配置。

以上一部分中的编译配置为例,假设您打算让“demo”版应用仅支持 API 级别 23 及更高级别。您可以使用 variantFilter 代码块过滤掉所有将“minApi21”和“demo”产品特性组合在一起的编译变体配置:

    android {
      ...
      buildTypes {...}

      flavorDimensions "api", "mode"
      productFlavors {
        demo {...}
        full {...}
        minApi24 {...}
        minApi23 {...}
        minApi21 {...}
      }

      variantFilter { variant ->
          def names = variant.flavors*.name
          // To check for a certain build type, use variant.buildType.name == "<buildType>"
          if (names.contains("minApi21") && names.contains("demo")) {
              // Gradle ignores any variants that satisfy the conditions above.
              setIgnore(true)
          }
      }
    }
    ...
    

将变体过滤器添加到编译配置,并点击通知栏中的 Sync Now 后,Gradle 会忽略符合您指定的条件的所有编译变体,并且当您从菜单栏中依次点击 Build > Select Build Variant(或点击工具窗口栏中的 Build Variants 图标 )时,它们不会再显示在下拉菜单中。

创建源集

默认情况下,Android Studio 会为您希望在所有编译变体之间共享的所有内容创建 main/ 源集和目录。但是,您可以创建新的源集来精确控制 Gradle 为特定版本类型、产品特性(以及使用类型维度时的产品特性组合)和编译变体编译和打包的文件。例如,您可以在 main/ 源集中定义基本功能,并使用产品特性源集来为不同客户端更改应用的品牌,或仅为使用“debug”版本类型的编译变体添加特殊权限和日志记录功能。

Gradle 要求您以某种类似于 main/ 源集的方式组织源集文件和目录。例如,Gradle 要求将“debug”版本类型特有的 Java 类文件放在 src/debug/java/ 目录中。

Android Plugin for Gradle 提供了一个有用的 Gradle 任务,向您展示如何整理每个版本类型、产品特性和编译变体的文件。例如,以下任务输出的示例描述了 Gradle 希望“debug”版本类型的部分文件所在的位置:

    ------------------------------------------------------------
    Project :app
    ------------------------------------------------------------

    ...

    debug
    ----
    Compile configuration: compile
    build.gradle name: android.sourceSets.debug
    Java sources: [app/src/debug/java]
    Manifest file: app/src/debug/AndroidManifest.xml
    Android resources: [app/src/debug/res]
    Assets: [app/src/debug/assets]
    AIDL sources: [app/src/debug/aidl]
    RenderScript sources: [app/src/debug/rs]
    JNI sources: [app/src/debug/jni]
    JNI libraries: [app/src/debug/jniLibs]
    Java-style resources: [app/src/debug/resources]
    

要查看此输出,请按以下步骤操作:

  1. 点击 IDE 窗口右侧的 Gradle 图标
  2. 依次转到 MyApplication > Tasks > android,然后双击 sourceSets。Gradle 执行该任务后,系统应该会打开 Run 窗口以显示输出。
  3. 如果显示内容不是处于如上所示的文本模式,请点击 Run 窗口左侧的 Toggle view 图标

注意:任务输出还为您展示了如何为要用于对应用运行测试的文件整理源集,例如 test/androidTest/ 测试源集

当您创建新的编译变体时,Android Studio 不会为您创建源集目录,而是为您提供一些有用的选项。例如,要仅为您的“debug”版本类型创建 java/ 目录,请执行以下操作:

  1. 打开 Project 窗格,然后从窗格顶部的下拉菜单中选择 Project 视图。
  2. 转到 MyProject/app/src/
  3. 右键点击 src 目录,然后依次选择 New > Folder > Java Folder
  4. Target Source Set 旁边的下拉菜单中,选择 debug
  5. 点击 Finish

Android Studio 会为您的“debug”版本类型创建一个源集目录,然后在其中创建 java/ 目录。或者,您也可以让 Android Studio 在您为项目添加特定编译变体的新文件时为您创建这些目录。例如,要为您的“debug”版本类型创建值 XML 文件,请执行以下操作:

  1. 在同一 Project 窗格中,右键点击 src 目录,然后依次选择 New > XML > Values XML File
  2. 输入 XML 文件的名称或保留默认名称。
  3. Target Source Set 旁边的下拉菜单中,选择 debug
  4. 点击 Finish

由于“debug”版本类型被指定为目标源集,因此 Android Studio 在创建 XML 文件时会自动创建必要的目录。生成的目录结构应如图 2 所示。

图 2. “debug”版本类型的新源集目录。

遵循相同的过程,您也可以创建产品特性的源集目录(如 src/demo/)和编译变体的源集目录(如 src/demoDebug/)。此外,您可以创建定位到特定编译变体(例如 src/androidTestDemoDebug/)的测试源集。要了解详情,请转到测试源集

更改默认源集配置

如果您的来源未按照 Gradle 预期的默认源集文件结构进行整理(如上文创建源集的相关章节中所述),则可以使用 sourceSets 代码块来更改 Gradle 为源集的每个组件收集文件的位置。您无需改变文件的位置,而只需向 Gradle 提供相对于模块级 build.gradle 文件的路径,Gradle 应该会在该路径下找到每个源集组件的文件。要了解您可以配置哪些组件,以及是否可以将它们映射到多个文件或目录,请阅读 Android Plugin for Gradle DSL 参考

以下代码示例将 app/other/ 目录中的源文件映射到 main 源集的某些组件,并更改 androidTest 源集的根目录。

    android {
      ...
      sourceSets {
        // Encapsulates configurations for the main source set.
        main {
          // Changes the directory for Java sources. The default directory is
          // 'src/main/java'.
          java.srcDirs = ['other/java']

          // If you list multiple directories, Gradle uses all of them to collect
          // sources. Because Gradle gives these directories equal priority, if
          // you define the same resource in more than one directory, you get an
          // error when merging resources. The default directory is 'src/main/res'.
          res.srcDirs = ['other/res1', 'other/res2']

          // Note: You should avoid specifying a directory which is a parent to one
          // or more other directories you specify. For example, avoid the following:
          // res.srcDirs = ['other/res1', 'other/res1/layouts', 'other/res1/strings']
          // You should specify either only the root 'other/res1' directory, or only the
          // nested 'other/res1/layouts' and 'other/res1/strings' directories.

          // For each source set, you can specify only one Android manifest.
          // By default, Android Studio creates a manifest for your main source
          // set in the src/main/ directory.
          manifest.srcFile 'other/AndroidManifest.xml'
          ...
        }

        // Create additional blocks to configure other source sets.
        androidTest {

          // If all the files for a source set are located under a single root
          // directory, you can specify that directory using the setRoot property.
          // When gathering sources for the source set, Gradle looks only in locations
          // relative to the root directory you specify. For example, after applying the
          // configuration below for the androidTest source set, Gradle looks for Java
          // sources only in the src/tests/java/ directory.
          setRoot 'src/tests'
          ...
        }
      }
    }
    ...
    

使用源集编译

您可以使用源集目录来添加只希望与某些配置打包在一起的代码和资源。例如,如果您要编译“demoDebug”这个变体(“demo”产品特性和“debug”版本类型的混合产物),则 Gradle 会查看这些目录,并为它们指定以下优先级:

  1. src/demoDebug/(编译变体源集)
  2. src/debug/(版本类型源集)
  3. src/demo/(产品特性源集)
  4. src/main/(主源集)

注意:如果您结合使用多个产品特性,那么这些产品特性的优先级由它们所属的类型维度决定。使用 android.flavorDimensions 属性列出类型维度时,属于您列出的第一个类型维度的产品特性的优先级高于属于第二个类型维度的产品特性,依此类推。此外,您为产品特性组合创建的源集的优先级高于属于各个产品特性的源集。

上面列出的顺序决定了 Gradle 组合代码和资源时哪个源集的优先级更高。由于 demoDebug/ 源集目录可能包含该编译变体特有的文件,因此,如果 demoDebug/ 包含在 debug/ 中也定义了的文件,则 Gradle 会使用 demoDebug/ 源集中的文件。类似地,Gradle 会为版本类型和产品特性源集中的文件提供比 main/ 中的相同文件更高的优先级。在应用以下编译规则时,Gradle 会考虑这种优先顺序:

  • java/ 目录中的所有源代码将一起编译以生成单个输出。

    注意:对于给定的编译变体,如果 Gradle 遇到两个或更多个源集目录定义了同一个 Java 类的情况,则会抛出编译错误。例如,在编译调试 APK 时,您不能同时定义 src/debug/Utility.javasrc/main/Utility.java。这是因为 Gradle 在编译过程中会查看这两个目录并抛出“重复类”错误。如果您要为不同的版本类型使用不同版本的 Utility.java,则可以让每个版本类型定义各自的文件版本,而不是将其包含在 main/ 源集中。

  • 所有清单都将合并为一个清单。优先级将按照上面列出的顺序提供。也就是说,版本类型的清单设置会替换产品特性的清单设置,依此类推。要了解详情,请阅读清单合并
  • 同样,values/ 目录中的文件也会合并在一起。如果两个文件(如两个 strings.xml 文件)的名称相同,将按照上面列表中的顺序指定优先级。也就是说,在版本类型源集的文件中定义的值会重写在产品特性的同一文件中定义的值,依此类推。
  • res/asset/ 目录中的资源会打包在一起。如果在两个或更多个源集中定义了同名的资源,将按照上面列表中的顺序指定优先级。
  • 最后,在编译 APK 时,Gradle 会为库模块依赖项随附的资源和清单指定最低优先级。

声明依赖项

您可以为特定编译变体或测试源集配置依赖项,方法是在 Implementation 关键字前面加上编译变体或测试源集的名称作为前缀,如以下示例所示。

    dependencies {
        // Adds the local "mylibrary" module as a dependency to the "free" flavor.
        freeImplementation project(":mylibrary")

        // Adds a remote binary dependency only for local tests.
        testImplementation 'junit:junit:4.12'

        // Adds a remote binary dependency only for the instrumented test APK.
        androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    }
    

有关详情,请参阅添加编译依赖项

配置签名设置

除非您明确定义此版本的签名配置,否则 Gradle 不会为该版本的 APK 签名。您可以轻松创建发布密钥并使用 Android Studio 为发布版本类型签名

要使用 Gradle 编译配置为您的发布版本类型手动配置签名,请执行以下操作:

  1. 创建一个密钥库。 密钥库是一个包含一组私钥的二进制文件。您必须将密钥库保存在安全可靠的地方。
  2. 创建一个私钥。 私钥代表将通过应用识别的实体,如个人或公司。
  3. 将签名配置添加到模块级 build.gradle 文件中:

        ...
        android {
            ...
            defaultConfig {...}
            signingConfigs {
                release {
                    storeFile file("myreleasekey.keystore")
                    storePassword "password"
                    keyAlias "MyReleaseKey"
                    keyPassword "password"
                }
            }
            buildTypes {
                release {
                    ...
                    signingConfig signingConfigs.release
                }
            }
        }
        

要生成已签名的 APK,请从菜单栏中依次选择 Build > Generate Signed APKapp/build/apk/app-release.apk 中的软件包现已使用您的发布密钥进行签名。

注意:在编译文件中添加发布密钥和密钥存储区的密码并不是一种好的安全做法。作为替代方案,您可以配置编译文件以从环境变量获取这些密码,或让编译流程提示您输入这些密码。

要从环境变量获取这些密码,请编写以下代码:

    storePassword System.getenv("KSTOREPWD")
    keyPassword System.getenv("KEYPWD")
    

要让编译流程在您要从命令行调用此编译时提示您输入这些密码,请编写以下代码:

    storePassword System.console().readLine("\nKeystore password: ")
    keyPassword System.console().readLine("\nKey password: ")
    

完成此流程后,您可以分发您的应用并在 Google Play 上发布它。

警告:请将密钥库和私钥保存在安全可靠的地方,并确保您为其创建了安全的备份。如果您将应用发布到 Google Play,随后丢失了用于为应用签名的密钥,那么您将无法向您的应用发布任何更新,因为您必须始终使用相同的密钥为应用的所有版本签名。

为 Wear OS 应用签名

发布 Wear OS 应用时,手表 APK 和可选的手机 APK 都需要进行签名。要详细了解如何打包 Wear OS 应用并为其签名,请参阅打包穿戴式设备应用