Gradle(一)

面对如此巨大的标题(Gradle),我只能说这是一篇关于他的心得体会。因为Gradle水太深了,智商完全被压制。

一、基本介绍

Gradle的核心就是在Groovy的领域特定语言(DSL),因为具有非常的扩展性,所以在面对大型的多项目构建它都能高效的提高构建任务。
Groovy作为动态语言,他和Java很像,因为都可以在Java虚拟机中运行。当运行Groovy脚本时它会先被编译成Java类字节码,然后通过JVM虚拟机执行这个Java字节码类。
所以部分基础还是和Java类似或使用Java借鉴的。当然作为Groovy最重要的还是闭包。

因为这里主要是记录的是Gradle的使用,所以要详细学习
Groovy语法:点击这里
Gradle语法:点击这里

在学习和使用Gradle时,建议在其API文档中加强学习。

二、Gradle在Android中的整体结构

在项目编译过程中会分为三个阶段:

初始化阶段:创建 Project 对象,如果有多个build.gradle,也会创建多个project,也就是我们的app或者module.
配置阶段:在这个阶段,会执行所有的编译脚本,同时还会创建project的所有的task,为后一个阶段做准备,这些task都会在build.gradle文件中。
执行阶段:在这个阶段,gradle 会根据传入的参数决定如何执行这些task,真正action的执行代码就在这里.

1.整体结构

首先来看下最基础的gradle在android上的结构
Project
|->app
| \—>build.gradle
|
|->module
| \->build.gradle
|
|->gradle
| \—>wrapper
|
|->.gradle
| \—>x.x
|
|->settings.gradle
\—>build.gradle

可以看到一个项目里会有一个根目录的settings.gradle、build.gradle文件,并且每个module里都会有一个自己的build.gradle文件

settings.gradle : 这个文件定义了哪些module要进行编译
顶层build.gradle : 项目顶层的build.gradle文件中的配置应用至所有的module。
module中build.gradle : 这是每个module的独有配置,他会覆盖顶层build.gradle中相同的配置。

2.build.gradle

1.setting.gradle

这个gradle文件中定义了哪些module会被添加到项目编译过程中。

2.顶层build.gradle

他的典型配置如下

buildscript {
repositories {
jcenter()

maven {
url "http://repo.acmecorp.com/maven2"
credentials {
username 'user'
password 'secretpassword'
}
}

ivy {
url "../local-repo"
}
}

flatDir {
dirs 'app/libs', 'app/arrs'
}

dependencies {
classpath 'com.android.tools.build:gradle:2.3.2'
}
}

allprojects {
repositories {
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

  • buildscript : 定义了Android编译工具的类路径。这里的jcenter是Maven仓库,之后我们还添加了maven仓库指定的url和他们用户名密码。当然我们还可以在这里指定我们的类库文件路径(常用的jar和aar文件路径)。
  • allprojects : 这里增加的配置会被针对到所有的module,因此我们一般不会再这里添加太多的东西。

3.module中build.gradle

他是每个module中单独的配置,如果这里的定义与顶层build.gradle定义相同,则后者会被覆盖。
以下代码中的功能后面会罗列出来。

apply plugin: 'com.android.application'

android {
compileSdkVersion 26
buildToolsVersion "26.0.1"
defaultConfig {
applicationId xxx
multiDexEnabled true
}
signingConfigs {
xt {
storeFile file("../Keys/key.jks")
storePassword ""
keyAlias ""
keyPassword ""
}
sr {
storeFile file("../Keys/key.jks")
storePassword ""
keyAlias ""
keyPassword ""
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
buildConfigField("String", "url", "\"release\"")
}
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
buildConfigField("String", "url", "\"debug\"")
}
staging.initWith(buildTypes.debug)
staging {
applicationIdSuffix ".staging"
versionNameSuffix "-staging"
buildConfigField("String", "url", "\"staging\"")
}
}
productFlavors {
xt {
signingConfig signingConfigs.xt
resValue("string", "app_name", "XT")
}
sr {
signingConfig signingConfigs.sr
resValue("string", "app_name", "SR")
}
cz {
signingConfig signingConfigs.sr
resValue("string", "app_name", "CZ")
}
}
dexOptions {
preDexLibraries = false
additionalParameters = [
'--multi-dex',
'--set-max-idx-number=30000',
'--main-dex-list='+projectDir+'/multidex.txt',
'--minimal-main-dex'
]
}
applicationVariants.all { variant ->
variant.outputs.each { output ->
def outputFile = output.outputFile
def buildTypeName = variant.buildType.name

if (outputFile != null && outputFile.name.endsWith(".apk")) {
def flavorsName = variant.productFlavors[0].name
def versionName = rootProject.ext.android.versionName

def fileName = "${flavorsName}_${buildTypeName}_${versionName}.apk"
output.outputFile = new File(outputFile.parent, fileName)
}
}

}
}

dependencies {
xtCompile project(':xx')
srCompile project(':yy')
czCompile project(':zz')
}

  • apply plugin : 这里应用了Android 程序的gradle插件,提供了Android 编译、测试、打包等等的所有task。
  • android : 关于Android 的所有配置基本都在这里,其中也包含了很多东西:

    defaultConfig : 这里是程序的默认配置,如果与AndroidManifest.xml的配置相同,则会以这里为主。applicationId作为程序的唯一识别ID,是为了防止在同一部手机装入两个一样的程序;另外就是我们的R文件会在这个包名下。而multiDexEnabled是作为我们的分包来配合使用的一个属性。
    signingConfigs : 用于打包用的认证签名,可以根据多个并分业务配置不同的认证签名。
    buildTypes : 定义了项目的编译类型,我们可以针对不同的编译配置选择和自定义编译类型,如自带的debug和release,最后还有自定义的staging的编译类型。
    productFlavors : 他可以完成同时生成多渠道多版本的操作。
    dexOptions : 用于处理分包来解决方法数大于65535及主dex过大的问题。

  • dependencies : 属于gradle 的依赖配置。它定义了当前项目需要依赖的其他库。

当然,这里的定义并不仅仅是这些,这里只是把会用到的罗列出来。

3.详细功能

1.批量打包

在使用eclipse还是AndroidStudio。都会对项目打包,而前期打包都会使用IDE的打包方式,很少用到指令。面对Debug版和Release版都需要进行打包并在切换打包类型时还需要在代码的摸个地方修改字段。(包括测试库与正式库的URL,部分功能的区分)这些都会在我们的项目中遇到。但是改代码是不到万不得已的决定,怎么办呢?
这里作为构建工具的优势就体现了出来,他会帮助我们来完成这些构建的定制化和自动化。

1.buildConfigField

下面这段代码可以清楚看到这里应该是创建了一个url的字段(后面的第三个参数暂且不管)。
第三个参数作为String的url,他的数据应该是“\“debug\“”或者是”\“release\“”。需要注意的是在双引号里面在加上字符转移,因为作为字符串的数据需要加上字符转移来正常显示我们数据的url=”debug”。
github

那么我们该如何用到这个数据呢:
github github
可以看到第一张图片中的左下角我们buidlVariant处于debug版本,所以在url中看到的是”debug模式”的字段。
而在第二张图片中的我们buidlVariant改为release版本,所以在url中看到的是”release模式”的字段。

这样就完成了对不同编译类型的代码环境与业务需求的基本分类。

2.resValue

当然,我们区分了debug与release的编译版本后,当然也希望修改资源的区别。这里的resValue就可以完成。
github
可以看到这里的app_name字段中为string资源(注意不是大写是小写)。(增加这项功能后会提示报错,需要将string资源中的同名字段去掉)

3.添加自定义编译类型

在正式开发中不仅仅是debug版本和release版本,为此,这里buildTypes可以自定义项目编译类型。
当你想在一台设备上安装多个构建成果, 如同时让debug和release版本共存。Android的项目区分必须以包名区分,也就是applicationId。在Gradle编译上可以使用applicationIdsuffix来区分相同的applicationId下的应用,他会在原有的项目包名下增加一个后缀以区分。
versionNameSuffix可以在查看项目版本名称时增加后缀。

android {

// ...
buildTypes {
debug {
//...
}
release {
//...
}
staging.initWith(buildTypes.debug)
staging {
// 项目下的application后缀名称
applicationIdSuffix ".staging"
versionNameSuffix "-staging"
buildConfigField("String", "url", "\"staging\"")
}
}
}

这里的自定义版本初始化是在debug版本的基础上进行的修改,也就是具有了debug的某些共性,但是在不同的地方如buildConfigField的数据上就会覆盖前者(debug)的版本数据。

这个时候我们在打包不仅会有debug版本和release版本,还会有staging版本。

4.指令打包

原有的打包可以选择debug和release版本进行打包,但这里只是两个,如果在添加多个编译版本和多渠道业务进行打包那就很多了(版本个数*渠道个数=需要打包的个数)。这里的使用指令打包可以进行所有打包,也可进行分类打包。

gradle assemble : 可以生成所有可以生成的项目包。由于打的包个数较多,所以度相对较慢。这里需要注意在打包前需要

在经过等待后提示build successful就说明打包完成了,那么包在哪儿呢?
github

可以看到一次生成了6个包,这里debug版本和release版本都生成了,要按类别生成apk则在assemble后面增加需要分类的名称。如:gradle assembleDebuggradle assembleRelease之外还可以根据多渠道的分类来进行打包。

5.自定义名称

在刚刚使用指令打包后会发现生成的apk的名称很像,还会有日期生成。这里我们是自定义的打包名称。
这里的${}我们可以看成字符串的拼接,无需理会。

applicationVariants.all { variant ->
variant.outputs.each { output ->
def outputFile = output.outputFile
// 编译类型名称
def buildTypeName = variant.buildType.name
if (outputFile != null && outputFile.name.endsWith(".apk")) {
// 多渠道类型名称(数组获取更多维度)
def flavorsName = variant.productFlavors[0].name
// 代码版本名称
def versionName = rootProject.ext.android.versionName

def fileName = "Demo_${flavorsName}_${buildTypeName}_${versionName}_${rootProject.ext.android.time}.apk"
output.outputFile = new File(outputFile.parent, fileName)
}
}
}

在最后的rootProject.ext.android.time,这里是一个在线日期的数据(此处代码编写至顶层gradle中):

ext {
android = [
versionCode : 1 ,
versionName : "1.0" ,
time : getTime()
]
}

def getTime() {
def date = new Date();
def time = date.format("yyyyMMdd")
return time
}

android类似一个map结合,使用上则是直接调用即可,后者是一个小方法,提供了当前的日期。
这里注意的是如果是写在顶层的gradle文件中,则不需要进行特殊调用。如果是写在单独的gradle中,如util.gradle自定义文件下,则需要添加文件引用apply from: “util.gradle”

注意:
在Gradle3.0中使用上述功能生成自定义名称会发生编译错误

//直接使用这个方法代替上述中的内部命名
outputFileName = "Cas_dp_${buildTypeName}_v${flavorName}_${versionName}_${rootProject.ext.buildTime}.apk"

6.产品多样化productFlavors构建

在项目打包中buildTypes中只是区分编译类型,当出现业务需求的区别或是手机型号的区分要求,则需要在这里进行处理。

可以看到这里添加了xt、sr、scan三个版本。里面也可以添加buildConfigFieldresValue等其他配置。于是我们在打包时分别生成以下所有版本(productFlavors)的所有编译版本(buildTypes)

android {
// ...
productFlavors {
xt {
applicationId ''
signingConfig signingConfigs.xt
buildConfigField("String", "url", rootProject.ext.urls.debug)
resValue("string", "app_name", "\\"x\\"")
}
sr {
applicationId ''
signingConfig signingConfigs.sr
buildConfigField("String", "url", rootProject.ext.urls.debug)
resValue("string", "app_name", "\\"s\\"")
}
scan {
applicationId ''
signingConfig signingConfigs.sr
buildConfigField("String", "url", rootProject.ext.urls.debug)
resValue("string", "app_name", "\\"c\\"")
}
}
}

完成上述代码并编译后就可以在项目中添加这些文件夹。可以看到整个文件夹和main一模一样,其中甚至还包含jniLibs。
github
可以使用相同包名,也可以使用在变体中添加的制定applicationId作为包名,此时就需要注意使用方法。

这里阐述一个相对复杂的情况:

  • 1.我们需要打出n个包。
  • 2.这n个包都会有对应绝对独立的单独需求、功能、依赖、jar包、so文件。
  • 3.每个包都需要做单独的sdk,所以需要将做成n个module。
  • 4.n随时增加,个数随需求随时变动,切保留之前的所有功能。

这里描述了应对上述问题的整个项目的大致结构:
github

为此,在不进行开发多个项目的情况下,使用Gradle构建管理工具可以完美的完成上述的问题。

解决办法:
第一、二个问题我们可以使用productFlavors来解决这个问题,生成多渠道来分别解决这类问题。为此需要对项目的重用性进行整体开发,我们这里并没有在productFlavors添加applicationId,就意味着我们的apk只能支持其中一个进行安装,如需修改则增加对应的applicationId即可。
这里因为没有applicationId,所以每个渠道的包名都是一致的,这也是为了解决R文件包名的问题。而so文件存放至每个渠道的项目文件夹中

第三个问题,我们可以看下图中的解决办法,在依赖的第三方包(jar包也可以)前加上渠道名称,之后的单词的第一个首字母大写就可以了(本人androidstudio 2.3.2)。
github
这里为每个渠道都使用了特定的module。这样在开发时这些第三方module不会相互影响对方的依赖,也很好的将产品多样化这个功能拆分的更加完善。

开始使用
首先我们要在gradle中添加productFlavors,并在里面添加需要增加的变体名称。
github

在同步后就可以在app/src下中添加与main同级的、与变体名称相对应的包名。注意这里并没有贸然增加包,这里会因工作需求来及时处理,是否需要同包名或是不同包名。(这里使用相同包名)
github

首先尝试修改不同变体的app名称及图标。
github
删除string中的同名字段后就可以使用了,可以打包试一试。在Terminal中输入gradle assemble指令并最后显示BUILD SUCCESSFUL表明打包成功,在项目的app/build/outputs/apk目录下看到已完成打包的apk。
之前也讲明生成的包一共是编译类型变体版本</font>,所以一共打了3*3=9个apk包。在安装后就可以看到我们显示的app的名称就是我们之前设定的resValue中的名称。
github

我们给每个版本增加了不同的资源来区别他们,这也提高了我们定制化的需求:
这里可以看到,我为每个版本都使用了不同的字符串资源及图片的显示。
github
之后可以看到app的显示效果,效果也很明显,我们需要的功能也体现了出来。
github

当然,面对上述个人描述为产品变体:面对需求或是功能上的产品分支
而仅仅有产品变体是不够面对众多需求的。因为这些产品在后期需要服务给指定的公司,因此还需要在每个产品上进行划分。这个时候Dimensions(产品规格)可以为我们解决问题。

注意:
在gradle3.0中需要对分渠道打包特殊对待,在gradle3.0下如果使用上述方法会发生编译错误,原因此时为了确认维度的统一性,必须在上述的功能基础上添加dimension(维度)功能,来避免flavor不会产生误差,因此即使只有一个维度,也需要增加其功能。

7.产品规格Dimensions构建

产品变体来分裂不同的公司,厂家,但是在公司中也会有不同的内部需求,此时就需要进行更深层次的产品多样性,因此在上述的产品多样性基础上添加了产品规格的一个概念:Dimensions(产品规格),官方以维度来解释这个功能,这里以产品规格来解释。特指在已有的产品上建立的另一类型区分。

android {
defaultConfig {
flavorDimensions "one", "two"
}
productFlavors {
base {
dimension "one"
}
Flavors1 {
dimension "two"
}
Flavors2 {
dimension "two"
}
}
}

这里可以看到在基础的base后面会根据规格来分类打包:base Flavors1, base Flavors2。
打包时可以使用variant.productFlavors[n].name来寻找自定义的规格。

dex分包

当app的方法数超过65535时生成的apk将无法安装,此时需要进行分包,在打包时将应用的代码分成多个dex,使主dex的方法数不超过系统限制。

android {
dexOptions {
javaMaxHeapSize "4g"
preDexLibraries = false
additionalParameters = [//dex参数详见 dx --help
'--multi-dex',//多分包
'--set-max-idx-number=60000',//每个包内方法数上限
'--main-dex-list='+projectDir+'/multidex.txt',//打包进主classes.dex的文件列表
'--minimal-main-dex'//使上一句生效
}
}

其中将需要添加的classes.dex的文件列表放在build.gradle同目录下的multidex.txt中
com/yang/test/main
com/yang/test/MainActivity.class