第四章 创建构建 Variant

第四章 创建构建 Variant

当开发一个应用时,通常会有几个不同的版本,不同版本的不同配置会让项目变得很复杂。
在 Gradle 中,有一些便捷和可扩展的概念可以用来定位这些常见问题。(定位!这些概念是用来描述问题的)。每个由 Android Studio 创建的新项目都会生成 debug 和 release 构建类型 buildtype。另外一个概念 product flavor,其让管理多个应用或依赖版本成为可能。
buildtype 和 product flavor 结合起来一起使用,可以很容易地处理各种版本的配置问题。结合起来的产物就叫做 构建 variant。

一、构建类型

在 Gradle 的 Android 插件中,构建类型通常被用来定义如何构建一个应用或依赖库。可以在 buildtypes 代码块中定义构建类型。

    buildTypes {
        release {
            //禁用清除无用的资源
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

注: 默认情况下,每个模块都有一个 debug 构建类型,其被设置为默认构建类型。debug 构建类型的 debuggable 属性为 true,其他构建类型的 debuggable 属性均为 false 。

1. 创建构建类型

新的构建类型,只需要在 buildTypes 代码块中新增一个对象即可:

        demo{
            // 给应用 id 添加个后缀
            applicationIdSuffix ".demo"
            // 给 版本名 添加个后缀
            versionNameSuffix "-demo"
            buildConfigField("String","API_URL","\"http://demo.xx.com/api\"")
        }

应用 id 不同,这意味着可以在同一个设备上同时安装 demo版本 和 release版本。
版本名 不同,这在同一个设备上区分多个应用版本时非常重要。

在新建一个构建类型A时,还可以用另一个构建类型B的属性来初始化构建类型A。(类似于继承)

        demo{
            initWith(buildTypes.debug)
            // 给应用 id 添加个后缀
            applicationIdSuffix ".demo"
            // 给 版本名 添加个后缀
            versionNameSuffix "-demo"
            buildConfigField("String","API_URL","\"http://demo.xx.com/api\"")
            debuggable false
        }

initWith() 方法创建了一个新的构建类型,并且复制一个已经存在的构建类型的所有属性到新的构建类型中(debug -> demo),我们在 demo 中定义了 debuggable 和 API_URL 来覆盖掉 debug 的属性。

2. 源集

我们知道源集指的就是一组会被一起执行和编译的源文件。
当创建一个新的构建类型时,Gradle 也会创建一个新的源集。

3. 依赖

每个构建类型都可以有自己的依赖。Gradle 自动为每个构建类型创建新的依赖配置。如果执行在 debug 构建中添加一个 logging 框架,可以使用 debugImplementation 关键字添加依赖。

二、product flavor

与被用来配置相同 APP 或 library 的不同构建类型相反,product flavor 被用来创建不同的版本。
如果需要一个全新的 APP ,独立于已有的应用发布,那么就使用 product flavor。否则使用构建类型即可。

1. 创建 product flavor

创建 product flavor 和创建 构建类型 类似。你可以通过在 productFlavor 代码块中添加新的 product flavor 来创建:

android{
    productFlavors{
        red{
            applicationId 'com.zyf.gradlevariant.red'
            versionCode 3
            flavorDimensions("why")
        }
    
        blue{
            applicationId 'com.zyf.gradlevariant.blue'
            versionCode 4
            minSdkVersion 14
            flavorDimensions("why")
        }
    }
}

ProductFlavor 和 DefaultConfig 均继承 BaseFlavor。

image

DefaultConfig 类只有一个构造方法,没有对 BaseFlavor 做任何其他的扩展。

image

ProductFlavor 类对 BaseFlavor 做了一些扩展。

/**
 * 封装此项目的所有  产品风格(product flavor)  的属性
 *
 * <p>如果你想在同一个 设备(谷歌商店或存储库) 中同时安装你项目的不同版本,那么就使用      
 * 产品风格(product flavor) 来表示。例如,你可以给你的应用配置 ‘demo’ 和 ‘full’ 
 * 这两个共享公共源码和资源的产品风格,可以为每种产品风格指定不同的功能、设备要求、资
 * 源、应用ID。所以,产品风格允许你通过简单的输出你项目的不同版本,仅仅改变他们之间不
 * 同的组件和设置。 
 *
 * <p>配置产品风格与配置 构建类型(build-type) 相似:把 productFlavors代码块 添
 * 加到模块中的 build.gradle 文件中,然后配置你需要的内容,产品风格支持与 
 * defaultConfig 同样的属性,因为 defaultConfig 实际上也是一个 ProductFlavor 
 * 对象,defaultConfig 这个 ProductFlavor 被用来作为其他 ProductFlavor 
 * 的基础配置。可以覆盖 defaultConfig 所有默认的属性,eg:applicationId 等
 * 
 * <p>当 Android 插件版本为 3.+ 时,每个 Flavor 都必须依附于一个 dimension
 *
 * <p>配置 product flavor 时,安卓插件会自动将 product flavor 与 buildtype 
 * 中的配置相关联来创建 guild variant (构建 variant)
 * 如果插件创建了你不行要的 variant ,则可以使用 VariantFilter 进行过滤
 */
public class ProductFlavor extends BaseFlavor {

    @Inject
    public ProductFlavor(
            @NonNull String name,
            @NonNull Project project,
            @NonNull ObjectFactory objectFactory,
            @NonNull DeprecationReporter deprecationReporter,
            @NonNull Logger logger) {
        super(name, project, objectFactory, deprecationReporter, logger);
    }

    private ImmutableList<String> matchingFallbacks;

    public void setMatchingFallbacks(String... fallbacks) {
        this.matchingFallbacks = ImmutableList.copyOf(fallbacks);
    }

    public void setMatchingFallbacks(String fallback) {
        this.matchingFallbacks = ImmutableList.of(fallback);
    }

    public void setMatchingFallbacks(List<String> fallbacks) {
        this.matchingFallbacks = ImmutableList.copyOf(fallbacks);
    }

    /**
     * 当一个明确的版本不与本地的模块依赖匹配时,插件应该尝试使用基于 product 
     * flavor 的分类列表
     *
     * <p>安卓插件3.+ 会尝试使用模块中的依赖匹配你模块中的每个 构建variant 。例如
     * 当你构建一个 freeDebug 版本的app时,插件会尝试将其与本地依赖模块的 
     * freeDebug 版本匹配。For example, when you build a "freeDebug" 
     * 
     * <p>然而,也可能会有这种情况,同样的 dimension(范围;维度) 既存在于app中,
     * 也存在于 app 的依赖库中。你的 app 包含一个 依赖库 不包含的 flavor。例如,
     * 假如你的 app 和 app 的依赖库中 包含一个 “tier” 的 风格维度(flavor 
     * dimension). app 中的 "tier" 维度包含 "free" 和 "paid" (两个 
     * flavor)。 但在同样的 dimension 下其中的一个依赖项只包含 “demo” 和 
     * “paid” 这两个 flavor(风格) 。当插件尝试构建 “free” 版本时,插件并不知道
     * 要用哪个依赖版本。此时会提示错误信息:
     *
     * <pre>
     * Error:Failed to resolve: Could not resolve project :mylibrary.
     * Required by:
     *     project :app
     * </pre>
     *
     * <p>在这种情况下,你应该使用<code>matchingFallbacks</code> 来指定匹配 "free" 产品风格的代替方案,如下所示:   
     * <pre>
     * 
     * // In the app's build.gradle file.
     * 
     * android {
     *     flavorDimensions 'tier'
     *     productFlavors {
     *         paid {
     *             dimension 'tier'
     *             // 因为依赖中已经包含了一个 “paid” 风格,该风格被包含在 
     *             // “tier” 维度中(dimension) ,你不必再为 “paid” 风格提供
     *             // 特定的属性了。
     *         }
     *         free {
     *             dimension 'tier'
     *             // 当依赖匹配维度中不包含一个 “free” 风格时,此处指定回退风格
     *             // 的排序列表,你想列举多少就列举多少,插件会选择排序列表中的
     *             // 第一个风格,使用在依赖的 “tier” 维度中
     * 
     *             // 这里就是一个数组而已,如果依赖中与我们列举的 flavor 出现不
     *             // 匹配的情况,那么就默认选择此处数组的第一个作为替代方案
     *             matchingFallbacks = ['demo', 'trial']
     *         }
     *     }
     * }
     * </pre>
     *
     * <p>注意,对于应用程序及其依赖库中存在的给定 风格维度 ,当库中包含app不包含的
     *  产品风格时,不会出现问题。  
     *
     * <p>如果你正在尝试解决:依赖库包含一个风格维度但是应用程序中不包含的问题。使用
     * missingDimensionStrategy() 方法。
     * @return 返回使用的 产品风格 名称 倒序排列
     */
    public List<String> getMatchingFallbacks() {
        if (matchingFallbacks == null) {
            return ImmutableList.of();
        }
        return matchingFallbacks;
    }

    @Override
    @NonNull
    protected DimensionRequest computeRequestedAndFallBacks(@NonNull List<String> requestedValues) {
        // 这个没看懂
                VariantManager.getModifiedName(getName()), ImmutableList.copyOf(requestedValues));
    }

    @Override
    protected void _initWith(@NonNull BaseConfig that) {
        super._initWith(that);

        if (that instanceof ProductFlavor) {
            matchingFallbacks = ((ProductFlavor) that).matchingFallbacks;
        }
    }
}

2. 源集

和构建类型类似,product flavor 也可以拥有它们自己的源集目录。为一个特殊的 flavor 创建一个文件夹就和创建一个有 flavor 名称的文件一样简单。
甚至可以为一个特定构建类型(buildtype)和 flavor 的结合体创建一个文件夹。该文件夹的名称将是 flavor名称+构建类型的名称。
例如,让 blue flavor 的 release 版本有一个不同的 应用图标,那么文件夹将会被叫做 blueRelease。
合并文件夹的组成将比构建类型文件夹和 product flavor 文件夹拥有更高优先级。

3. 多种定制的版本

在某些情况下,我们可能需要创建 product flavor 的结合体。例如:客户 A 和客户 B 在他们的 APP 中都想要免费版和付费版,并且是基于相同的代码、不同的品牌。创建四种不同的 flavor 意味着需要像这样设置多个拷贝,这并不是最佳做法。使用 flavor dimension 是结合 flavor 的有效方式:

android{
    flavorDimensions "color","price"
    productFlavors{
        red{
            dimension "color"
        }
        blue{
            dimension "color"
        }
        free{
            dimension "price"
        }
        paid{
            dimension "price"
        }
    }
}

如果为 flavor 添加了维度,Gradle 会希望我们为每个 flavor 都添加维度。如果你忘了,那么你会收到一个带有错误接受的构建错误。 flavorDimensions 数组定义了维度,维度的顺序非常重要。当结合两个 flavor 时,它们可能定义了相同的属性或资源。在这种情况下,flavor 维度数组的顺序就决定了哪个 flavor 配置将被覆盖。在上一个例子中,color 维度覆盖了 price 维度,该顺序也决定了 构建 variant 的名称。

假设默认的构建配置是 debug 和 release 构建类型(buildtype),那么上述例子中定义的 flavor 将会生成下面这些 构建 variant:

  • blueFreeDebug and blueFreeRelease
  • bluePaidDebug and bluePaidRelease
  • redFreeDebug and redFreeRelease
  • redPaidDebug and redPaidRelease

三、构建variant

构建 variant 是 构建类型 和 product flavor 结合的结果。不论什么时候创建一个 构建类型 或 product flavor,新的 variant 都会被创建。
例子:如果你有标注的 debug 和 release 构建类型,并且你创建了一个 red 和 blue 的 product flavor ,那么会生成下图所示的 构建 variant:

image

公式: flavorDimensions 中每个维度含有的 flavor 的乘积 再乘以 buildtype 的数量。
( color 含有 2 个 flavor ,price 含有 2 个 flavor ,2 * 2 = 4 。再乘以 buildtype 的数量 * 2 = 8)

在上述窗口选择 variant 后,点击

image
就会运行对应的 variant。(如果没有 product flavor,则 variant 只包含构建类型)。

image

1. 任务

Gradle 的 Android 会为我们配置的每一个构建 variant 创建任务。

一个新的 Android 应用默认有 debug 和 release 两种构建类型,所以你可以用 assembleDebug 和 assembleRelease 来分别构建两个 APK。即用单个命令 assemble 来创建两个 APK。

当添加一个新的构建类型时,新的任务也将被创建。一旦你开始添加 flavor,那么整个全新的任务系列将会被创建,因为每个构建类型的任务会和每个 product flavor 相结合。这意味着,仅用一个构建类型和一个 flavor 做一个简单设置,你就会有三个任务用于创建全部 variant:

  • assembleBlue:使用 blue flavor 配置和组装 BlueRelease 及 BlueDebug
    • image
    • image
  • assembleDebug:使用 debug 构建配置类型,并为每个 product flavor 组装一个 debug 版本
    • image
    • image
  • assembleBlueDebug:用构建配置类型(buildtype)集合 flavor 配置,并且 flavor 设置将会覆盖构建类型(buildtype)设置
    • image
    • task的名称是根据 buildVariant 窗口确定的
    • image
    • 所以应该输入的是:assemble{颜色}{价格}{buildType} ,一定要按照顺序!

新的 tasks 是为每个 构建类型、每个 product flavor、每个 构建类型和product flavor结合体 创建的。

2. 源集

构建variant,是一个 buildType 和 一个或多个 product flavor 的结合体,其可以有自己的 源集文件夹。
例如,由 debug buildType blue flavor 和 free flavor 创建的 variant 可以有其自己的源集 src/blueFreeDebug/java/。其可以通过使用 sourceSet 代码块来覆盖文件夹的位置。

3. 源集合并资源和 manifest

源集的引入额外增加了构建进程的复杂性。
Gradle 的 Android 插件在打包应用之前将 main 源集和 buildType 源集合并在一起。此外,library 项目也可以提供额外的资源,这些也需要被合并。这同样适用于 manifest 文件。
例如:在你应用的 debug variant 中可能需要额外的 Android 权限来存储 log 文件。但你并不想在 main 源集中申请该权限,因为这样会吓跑潜在客户。相反,你可以在 debug buildType 的源集中额外添加一个 manifest 文件来申请额外的权限。
资源和 manifest 的优先顺序:

image

如果资源在 flavor 和 main 源集中都有申明,那么 flavor 中的资源将被赋予更高的优先级,那么就会打包 flavor 中的资源。

4. variant 过滤器

在你的构建中,可以完全忽略某些 variant。通过这种方式,你就可以通过 assemble 命令来加快构建所有 variant 的进程,并且你的任务列表不会被任何无须执行的任务污染。
variant 过滤器也可确保在 Android Studio 的构建 variant 切换器中不会出现过滤的构建 variant:

android{
    android.variantFilter { variant ->
        if (variant.buildType.name == 'release') {
            //说明是发布版
            variant.getFlavors().each { flavor ->
                //遍历发布版的风格
                if(flavor.name == 'blue'){
                    //说明是 蓝色发布版
                    //那么就把它给过滤了
                    variant.setIgnore(true)
                }
            }
        }
    }
}

现象:
image

四、签名配置

我们在发布应用之前,会使用 私钥 给它签名。
如果你有一个付费版和免费版或针对不同用户的不同应用,那么你需要为每个 flavor 使用不同的 私钥 签名。这就是签名配置出现的原因:

android{
       //签名配置
    signingConfigs{
        demo.initWith(signingConfigs.debug)

        //构建文件中的对象都是从上到下顺序创建的
        //所以下面这个构建类型release中使用到的signingConfigs.release对象
        //要先创建,才能被使用
        //所以要将signingConfigs{}放在buildTypes{}上面
        release{
            storeFile file("release.keystore")
            storePassword "woshimima"
            keyAlias "gradleforandroid"
            keyPassword "secretpassword"
        }
    }
}

Android 插件使用了一个通用 keystore 和一个已知密码自动创建了 debug 配置,所以就没必要为该构建类型再创建一个签名配置了。
demo 配置时,使用了 initWith(),其会从另一个签名配置中复制所有属性,这意味着 demo 构建是通过 debug 密钥签名的,并不是我们自定义的。
release 配值时,使用了 storeFile 来指定 keystore 文件的路径,之后定义了密钥别名和两个密码。

住: 最好不要在 Gradle 构建文件中存储凭证,因为 Gradle 的构建文件会被上传到版本控制系统,最好的方式是使用 Gradle 配值文件。

第七章会完整介绍如何存储签名配置密码

在定义签名配置之后,需要将它们应用到 构建类型 或 flavor 中。构建类型 和 flavor 都有一个叫做 signingConfig 的属性:

android{
    buildTypes{
        release {
            signingConfig signingConfigs.release
        }
    }
}

上述例子使用了构建类型,如果你想为每个你所创建的 flavor 使用不同的凭证,那么你就需要创建不同的签名配置,也就是说给每个 flavor 的 signingConfig 属性赋值。

使用签名配置这种方式会造成很多问题,当为一个 flavor 分配一个配置时,实际上它是覆盖了 buildType 的签名配置。如果你不行这样,那么在使用 flavor 时,就应该为每个 buildType 的每个 flavor 分配不同的密钥,而这是可以直接在 buildType 中完成的:

android{
    //在signingConfigs中定义了两个对象:red、blue
    //注:要把signingConfigs,写在 buildTypes 前面
    signingConfigs{
        red{
        
        }
        
        blue{
        
        }
    }

    buildTypes{
         release {
            signingConfig signingConfigs.release

            //给release版本的 red flavor 配置签名 red
            productFlavors.red.signingConfig signingConfigs.red
            
            //给release版本的 blue flavor 配置签名 red
            productFlavors.blue.signingConfig signingConfigs.blue
        }
    }
}

上述例子展示了在不影响 debug 和 demo 构建类型的情况下,如何在 release 构建类型中为 red flavor 和 blue flavor 使用不同的签名配置。

五、总结

构建类型是什么( buildType )、product flavor 是什么。
buildType 和 product flavor 的结合体:构建 variant。
如果需要相同代码的项目,在同一设备中安装两个应用,那么需要用到 product flavor。
构建 variant 就是多渠道打包的关键概念。

签名配置是什么。

一定要注意在构建文件中每个代码块的书写顺序!

下一章:管理多模块构建。

最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容