Gradle for Android(四) 创建构建变体

开发应用时, 通常会有几个不同的版本。最常见的是有一个测试用的临时版本和一个生产版本。这些版本通常有不同的设置,比如不同的URL地址等。除此之外,你可能有一个免费版和一个包含更多功能的付费版。这种情况下,你已经有了四个不同的版本:临时免费版,临时付费版,生产免费版,生产付费版。不同的版本有不同的配置是非常复杂的事情。

Gradle有一些规则和可扩展的概念来解决这一问题。我们已经提及Android Studio为每个新建工程创建的debugrelease两种build types。这是另一个称为product flavors的概念,它可以管理一个app或者library的不同版本。build typesproduct flavors是结合起来使用的,这就使管理免费和付费版的临时和生产应用变得简单。build typeproduct flavor的结合称为build variant(构建变体)。

本章我们首先学习build types,看看它如何使开发者的工作更加简单。接下来我们会讨论build typesproduct flavors的不同以及如何使用它们。我们还会学习发布app所必需的signing configurations(签名配置),以及如何为每个构建变体设置不同的签名配置。

本章我们学习如下内容:

  • build types
  • product flavors
  • build variants
  • signing configurations

Build types(构建类型)

在Gradle Android插件中,构建类型用来定义一个app或者library应该如何构建。每个构建类型可以指定是否包含调试符号,applicationId是什么,无用的资源是否应该移除等等。你可以使用buildTypes块来定义构建类型。下面是Android Studio创建的标准buildTypes块:

android {
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

新模块默认的build.gradle文件配置了一个名为release的构建类型。它只是禁用了移除无用的资源(通过设置minifyEnabledfalse),并且定义了ProGuard配置文件的位置。这是为了让开发人员可以直接使用ProGuard进行生产构建。

release构建类型不是唯一一个自动为你的工程创建好的构建类型。每个模块默认还有一个debug构建类型。它设置有合理的默认值,你也可以在buildTypes块中配置它,覆写属性值。

debug构建类型拥有它自己的默认设置,以使它方便调试。当你创建自己的构建类型时,会有不同的默认值。比如,debug构建类型的debuggable属性设置为true,而你自己创建的其他类型为false

创建构建类型

当默认设置不满足需求时,你也可以很容易地创建自定义构建类型。你需要做的只是在buildTypes块中添加一个新的构建类型的对象。下面创建一个staging的构建类型:

android {
    buildTypes {
        staging {
            applicationIdSuffix ".staging"
            versionNameSuffix "-staging"
            buildConfigField "String", "API_URL","\"http://staging.example.com/api\""
        }
    }
}

staging构建类型定义了一个新的applicationId的后缀,这样就和debugrelease版本区分开来。假设其他配置使用默认值,这样每个构建类型的applicationId如下:

  • Debug:com.package
  • Release:com.package
  • Staging:com.package.staging

这意味着你可以在设备上同时安装release版本和staging版本,而不会引起冲突。staging构建类型也有一个版本名称的后缀,用于区分同一设备不同版本的app。buildConfigField属性定义了一个URL地址,我们在第二章已经见过。

在创建构建类型时,你不必完全自定义,你也可以从由其他构建类型来初始化。

android {
    buildTypes {
        staging.initWith(buildTypes.debug)
        staging {
            applicationIdSuffix ".staging"
            versionNameSuffix "-staging"
            debuggable = false
        }
    }
}

initWith()方法会根据传入的构建类型来初始化当前构建类型。你可以在新的构建类型对象中覆写属性,或者定义新的属性。

Source sets(源码集)

在你创建一个新构建类型时,Gradle也会创建一个新的源码集。源码集目录默认和构建类型同名,但并不会自动创建。你需要手动创建该目录才能为该构建类型自定义源代码和资源文件。

下面是标准的debugrelease构建类型,外加自定义的staging构建类型的源码集目录结构:

app
└── src
    ├── debug
    │    ├── java
    │    │   └── com.package
    │    │         └── Constants.java
    │    ├── res
    │    │  └── layout
    │    │         └── activity_main.xml
    │    └── AndroidManifest.xml
    ├── main
    │    ├── java
    │    │    └── com.package
    │    │            └── MainActivity.java
    │    ├── res
    │    │    ├── drawable
    │    │    └── layout
    │    │            └── activity_main.xml
    │    └── AndroidManifest.xml
    ├── staging
    │    ├── java
    │    │    └── com.package
    │    │            └── Constants.java
    │    ├── res
    │    │    └── layout
    │    │            └── activity_main.xml
    │    └── AndroidManifest.xml
    └── release
         ├── java
         │     └── com.package
         │          └── Constants.java
         └── AndroidManifest.xml

这些源码集带来巨大的便利,比如你可以为某个构建类型覆盖特定属性,添加特定的代码,添加特殊的布局文件或者字符串资源等。

在你为每个构建类型添加Java类的时候,需要知道这是互斥的。这意味着如果你在staging的源码集中添加了CustomLogic.java文件,你可以在debug或者release源码集中添加同一个文件,但是不可以在main中添加,否则该文件会被定义两次,从而在构建时引发异常。

资源文件的处理方式和代码文件不同。图片和布局文件会覆盖main中的同名文件,但是values目录下的文件(比如strings.xml)不会。Gradle会将该构建类型和main中的values目录下的文件进行合并。

举个例子,假如main中的strings.xml文件如下:

<resources>
    <string name="app_name">TypesAndFlavors</string>
    <string name="hello_world">Hello world!</string>
</resources>

staging中的strings.xml如下:

<resources>
    <string name="app_name">TypesAndFlavors STAGING</string>
</resources>

合并后的strings.xml文件为:

<resources>
    <string name="app_name">TypesAndFlavors STAGING</string>
    <string name="hello_world">Hello world!</string>
</resources>

以上规则同样适用于manifest文件。当你为一个构建类型创建manifest文件时,你不必从main中将整个manifest文件拷贝过来,你只需要添加你需要的标签就可以了。Android插件会将它们合并到一起。

本章的后续内容我们会详细讨论文件合并。

Product flavors

构建类型用来为同一个app或者library配置不同的构建,与之相反,product flavors用来为同一个app创建不同的版本。典型的例子是一个app有一个免费版本和一个付费版本。另一个常见的场景是一个代理程序,它为几个客户构建具有相同功能的应用程序,只是品牌发生改变。一个公司制作的应用可以被同类型的客户重复使用,这在出租车行业和银行领域是很常见的。改变的仅仅是颜色,logo,后台地址。Product flavors极大简化了创建基于同样代码的不同版本的应用的过程。

如果你不确定自己需要新的build type还是新的product flavor,你应该问问自己是想要为了内部使用为应用创建一个新的构建,还是想要向Google Play发布一个新的APK。如果你想要在原有的基础上创建一个全新的可单独发布的应用,你应该选择product flavors,否则,你应该使用build types。

创建product flavors

创建product flavors和创建build types十分相似。你可以通过添加productFlavor块来实现:

android {
    productFlavors {
        red {
            applicationId 'com.gradleforandroid.red'
            versionCode 3
        }
        blue {
            applicationId 'com.gradleforandroid.blue'
            minSdkVersion 14
            versionCode 4
        }
    }
}

Product flavors和build types相比有不同的属性。这是因为product flavors是ProductFlavor类的对象,就像所有构建脚本中的defaultConfig对象一样。这意味着defaultConfig和你所有的product flavors拥有同样的属性。

Source sets

和构建类型一样,product flavors也可以有自己的源码集目录。你只需要创建一个和flavor同名的文件夹即可。你甚至可以更进一步,创建一个build type和flavor名称结合起来的目录。名称结合时,flavor在前,build type在后。比如,你想为release版本,blueflavor的app设置一个不一样的图标,则结合后的源码集目录为blueRelease。该目录优先级高于单独的release目录或者blue目录。

复合的flavor变体

有些时候,你想要更进一步创建product flavor的组合。举个例子,客户A和客户B想同时拥有免费和付费版本的应用,并且两个客户的商标不同。创建四个不同的flavors会导致重复的设置。这时,组合flvors是一个高效的方式,只需要为flavor设置不同的维度就可以了:

android {
    flavorDimensions "color", "price"

    productFlavors {
        red {
            flavorDimension "color"
        }
        blue {
            flavorDimension "color"
        }
        free {
            flavorDimension "price"
        }
        paid {
            flavorDimension "price"
        }
    }
}

一旦使用了flavorDimensions,你需要为每个flavor指定一个维度,否则会产生编译错误。flavorDimensions定义了一组维度,它们的顺序很重要。在组合两个flavors时,它们可能定义了相同的属性或者资源。这种情况下,维度的顺序决定了优先使用哪个flavor的配置。在上面的例子中,color维度会覆盖price维度。维度的顺序也决定了构建变体的名称。上例产生的构建变体如下:

  • blueFreeDebugblueFreeRelease
  • bluePaidDebugbluePaidRelease
  • redFreeDebugredFreeRelease
  • redPaidDebugredPaidRelease

Build variants(构建变体)

构建变体是build types和product flavors简单组合的结果。每当你创建一个新的build type或者product flavor,一个新的变体也会被创建。比如,你有标准的debugrelease构建变量,你又创建了redblue两个product flavors,这时将会创建下面的构建变体:

<center>
图1 Android Studio构建变体窗口

图1 Android Studio构建变体窗口</center>

这是Android Studio的Build Variants窗口的截图。该窗口列出了所有的构建变体,并允许你切换它们。点击Run按钮将会运行你所选择的构建变体。

如果你没有product flavors,构建变体将只包含构建类型。你不可以没有任何构建类型。即使你没有定义自己的构建类型,Android插件也会为app或者library创建debug构建类型。

依赖

每个构建类型有它自己的依赖。Gradle自动为每个构建类型创建一个新的依赖配置。如果你只想为debug版本添加一个日志框架,可以这样做:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.2.0'
    debugCompile 'de.mindpipe.android:android-logging-log4j:1.0.3'
}

你可以用这种方式将构建类型和依赖配置组合起来使用,比如stagingProvidedreleaseApk等。这使你可以得到非常具体的依赖项。

Product flavor也可以有自己的依赖配置,定义方式同构建类型,如freeCompile等。

然而如果你想为一个包含构建类型和Product flavor的构建变体添加依赖配置,你需要在configurations块中初始化这个配置。下面为freeDebug变体添加apk依赖:

configurations {
    // Initializes a placeholder for the freeDebugApk dependency configuration.
    freeDebugApk {}
}

dependencies {
    freeDebugApk fileTree(dir: 'libs', include: ['*.jar'])
}

任务

Gradle Android插件会为每个构建变体创建任务。新建的Android应用默认有debugrelease构建类型,所以你可以使用assembleDebugassembleRelease任务来创建对应的APK,或者使用assemble任务同时创建两个。当你添加新的构建类型时,新的任务也会被创建。一旦你添加了flavors,一系列新的任务也会被创建。因为构建类型的任务需要和product flavor的任务结合起来。这意味着对于只有一个构建类型和一个flavor的简单配置,你已经有了三个任务来构建所有的构建变体:

  • assembleBlue使用blue flavor的配置,同时assemble BlueReleaseBlueDebug
  • assembleDebug使用debug构建类型的配置,为每个product flavor assemble一个debug版本。
  • assembleBlueDebug结合了blue flavor和debug构建类型的配置,并且flavor设置会覆盖构建类型的设置。

Source sets

由一个构建类型和一个或多个product flavor组成的构建变体也可以有自己的源码集目录。比如由debug构建类型,blue flavor和free flavor组成的构建变体,源码集目录为src/blueFreeDebug/java/。你也可以在sourceSets块中修改目录位置,这在第一章出现过。

合并资源和manifest文件

源码集的引入增加了构建过程的复杂度。Gradle Android插件在打包app前需要合并main源码集和构建类型的源码集。除此之外,库工程也可能提供额外资源,它们也需要合并进来。这同样适用于manifest文件。比如,你在debug变体的app中可能需要额外的Android权限来保存日志文件,而你并不想在main源码集中声明这些权限,因为这可能引起潜在用户的抵触。作为替代方案,你需要在debug构建类型的源码集中添加manifest文件来声明这些权限。

资源和manifest文件的优先级顺序如下:

Build type > Flavor > Main > Dependencies

如果一个资源同时在flavor和main的源码集中声明,那么flavor中的资源有更高的优先级。这种情况下,flavor源码集中的资源会被打包,main源码集中的不会。库工程中声明的资源优先级最低。

合并资源和manifest文件还有许多需要学习的地方。如果你想学习更多细节,可以阅读官方文档:http://tools.android.com/tech-docs/new-build-system/user-guide/manifest-merger

创建构建变体

Gradle可以很容易地处理复杂的构建变体。即使是创建并配置两个构建类型和两个product flavors,构建文件依然很简洁:

android {
    buildTypes {
        debug {
            buildConfigField "String", "API_URL","\"http://test.example.com/api\""
        }

        staging.initWith(android.buildTypes.debug)
        staging {
            buildConfigField "String", "API_URL","\"http://staging.example.com/api\""
            applicationIdSuffix ".staging"
        }
    }

    productFlavors {
        red {
            applicationId "com.gradleforandroid.red"
            resValue "color", "flavor_color", "#ff0000"
        }

        blue {
            applicationId "com.gradleforandroid.blue"
            resValue "color", "flavor_color", "#0000ff"
        }
    }
}

本例我们创建了四个构建变体:blueDebug,blueStaging,redDebugredStaging

变体过滤器

你也可以在构建中完全忽略某个变体。这样,你就可以加快assemble命令构建所有变体的速度,并且去掉不用执行的任务。这也可以保证在Android Studio的构建变体窗口(见图1)中不会出现这个变体的选项。

你可以在build.gradle文件中添加如下代码来过滤掉构建变体:

android.variantFilter { variant ->
    if(variant.buildType.name.equals('release')) {
        variant.getFlavors().each() { flavor ->
            if (flavor.name.equals('blue')) {
                variant.setIgnore(true);
            }
        }
    }
}

本例中,我们首先检查变体的构建类型是不是release,然后我们检查该构建类型的所有flavors。getFlavors()方法返回一个flavor数组,数组的长度等于flavor的维度数。比如,对于blueFreeDebug变体而言,flavor数组包含bluefree两个flavor。本例过滤掉了blueFreeReleasebluePaidRelease两个变体。运行gradlew tasks命令,将不会看到这两个变体相关的任务。

签名配置

在你将应用发布到Google Play或其他应用市场之前,你需要用私钥进行签名。如果你有多个不同的版本,你需要为每个flavor使用不同的私钥进行签名。这就需要使用签名配置了。

android {
    signingConfigs {
        staging.initWith(signingConfigs.debug)

        release {
            storeFile file("release.keystore")
            storePassword"secretpassword"
            keyAlias "gradleforandroid"
            keyPassword "secretpassword"
        }
    }
}

本例我们创建了两个不同的签名配置。

Android插件会默认配置一个名为debug的签名配置,使用一个通用的公开密码的keystore文件,所以没有必要再为debug构建类型创建一个签名配置。

staging配置调用了initWith()函数,该函数通过传入的配置初始化当前配置。这就表示staging使用了和debug相同的签名配置。

release配置使用storeFile来指定keystore文件的路径,同时定义了key别名和用到的密码。

就像前面提到的,在构建文件中保存证书不是很好的设计。推荐保存在Gradle properties文件中。第七章会用一大块去讲解处理签名配置密码的一个任务。

在定义了签名配置之后,你需要将其应用到构建类型或者flavor中。构建类型和flavor都有一个名为signingConfig的属性:

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

本例使用了构建类型。如果你想为每个flavor使用不同的证书,你需要创建不同的签名配置。你可以用同样的方式定义它们:

android {
    productFlavors {
        blue {
            signingConfig signingConfigs.release
        }
    }
}

通过这种方式使用签名配置会出现问题。为flavor设置签名配置时,会覆盖构建类型的签名配置。更好的方式是为每个构建类型每个flavor设置不同的配置:

android {
    buildTypes {
        release {
            productFlavors.red.signingConfig signingConfigs.red
            productFlavors.blue.signingConfig signingConfigs.blue
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,812评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,626评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,144评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,052评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,925评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,035评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,461评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,150评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,413评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,501评论 2 307
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,277评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,159评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,528评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,868评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,143评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,407评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,615评论 2 335

推荐阅读更多精彩内容

  • 当你在开发一个app,通常你会有几个版本。大多数情况是你需要一个开发版本,用来测试app和弄清它的质量,然后还需要...
    justCode_阅读 407评论 0 2
  • 第四篇( 构建变体 ) 当你在开发一个app,通常你会有几个版本。大多数情况是你需要一个开发版本,用来测试app和...
    一剑飞鸿阅读 652评论 0 0
  • 转载注明出处:http://www.jianshu.com/p/5255b100930e 0. 前言 完全由个人翻...
    王三的猫阿德阅读 2,483评论 0 4
  • 1.介绍 如果你正在查阅build.gradle文件的所有可选项,请点击这里进行查阅:DSL参考 1.1新构建系统...
    Chuckiefan阅读 12,094评论 8 72
  • 当你在开发一个app,通常你会有几个版本。大多数情况是你需要一个开发版本,用来测试app和弄清它的质量,然后还需要...
    雪残阅读 406评论 0 0