Gradle介绍
Gradle 是以 Groovy 语言为基础,面向Java应用为主。基于DSL(领域特定语言)语法的自动化构建工具。
Gradle可以自动化地进行软件构建、测试、发布、部署、软件打包,同时也可以完成项目相关功能如:生成静态网站、生成文档等。
Gradle集合了Ant的灵活性和强大功能,同时也集合了Maven的依赖管理和约定。并且特有的DSL,使得代码简洁,易懂。开发起来更加方便,复用性高。
两个基本概念:项目和任务
- 项目
- 任务
那这些概念和Gradle的构建又有什么联系?。好,每一次Gradle的构建都包含一个或多个项目。
下面这张图就很好的说明了他们之间的联系
.
这个图是我从我们项目中截取下来的。 红色部分表示和Gradle相关的文件。我会给大家说明这几个文件都是干嘛的。
- Gradle构建脚本(build.gradle)它指定了一个项目和它的任务, 也可以说是项目最主要的gradle配置文件。有一个项目就会有一个build.gradle
- Gradle属性文件(gradle.properties)用来配置构建属性。
- Gradle设置文件(gradle.settings)对于只有一个项目的构建而言是可选的,如果我们的构建中包含多于一个项目,那么它就是必须的,因为它描述了哪一个项目参与构建。每一个多项目的构建都必须在项目结构的根目录中加入一个设置文件。 对于我们的项目来说它是必须的,因为项目中有 APP base haodf。三个项目。
来看settings.gradle的内容,
只有一行代码。它通过include的函数,将子项目包含进来。另外,setting.gradle除了可以include外,还可以设置一些函数。这些函数会在gradle构建整个工程任务的时候执行,所以,可以在settings做一些初始化的工作。比如: 创建特定的目录
在来看gradle下的wapper文件夹,这里我们主要看下gradle.wrapper.properties。
它里面声明了gradle的目录与下载路径。
它所指定的文件位置在这:也就是你当前用户下的.gradle文件中。如果这个路径下没有gradle,它就会去下载,但是这个网站访问比较慢,最好是自己去下载然后考到这个目录中。
这两个文件做了什么事情呢:
- 解析 gradle/wrapper/gradle-wrapper.properties 文件,获取项目需要的 gradle 版本下载地址
- 判断本地用户目录下的 ./gradle 目录下是否存在该版本,不存在该版本,走第3点,存在走第4点
- 下载 gradle-wrapper.properties 指定版本,并解压到用户目录的下 ./gradle 文件下
- 利用 ./gradle 目录下对应的版本的 gradle 进行相应自动编译操作
至于build.gradle 和 gradle.properties中的内容我们放到后来说。
构建第一个Gradle
新建一个文件build.gradle, 然后添加如下代码:
task hello {
doLast {
println 'Hello world!'
}
}
在命令行里,进入包含build.gradle的文件夹。然后通过命令 gradle -q hello 执行构建脚本:
输出
> gradle -q hello
Hello world!
这个构建脚本定义了一个单独的task,叫做hello,并且加入了一个action,当你运行gradle hello。
gradle执行叫做hello的task,也就是执行了你所提供的action,Action,对应的就是task里面具体的有一个操作。一个task可以包含多个Action,它有doFirst和doLast两个函数,用于 添加需要最先执行的 Action 和需要和需要最后执行的 Action。这个操作其实就是一个用Groovy代码写的闭包,代码中的task是Project类里的一个方法,通过调用这里的task方法创建了一个Task对象,并在对象的doLast方法中传入println 'Hello, Gradle!'这个闭包。这个闭包就是一个Action。
它还有一种简易写法就是
task hello << { // 等同于doLast
println 'Hello world!'
}
得到的结果和上面是一样的。
任务依赖
task hello << {
print 'Hello, '
}
task intro(dependsOn: hello) << {
println "Gradle!"
}
这里定义了两个任务,第一个任务 输出“Hello" ,然后第二个任务依赖第一个任务并输出"Gradle"。结果打印的就是"Hello Gradle"。
task intro(dependsOn: 'hello') << {
println "Gradle!”
}
task hello << {
print 'Hello, '
}
被依赖的task不必放在前面声明,在后面也是可以的,这一点对之后讲到的多任务构建是非常重要的。
小结:
通过上面的介绍我们可以了解到:
- 每一个Project都必须设置一个build.gradle。
- 对于多项目构建,需要在根目录下也放一个build.gradle和一个setting.gradle
- 一个Project是由若干tasks来组成,当执行gradle的时候实际是执行写好的任务。
- 一个任务可以依赖多个任务。执行任务前会先执行依赖的任务。
Gradle命令介绍
gradle projects 查看工程信息。
你可以查看整个工程中的项目结构。
gradle tasks 查看任务信息
它展示了整个工程中所有Task的信息
gradle taskName(任务名)
上面中所展示的任务都可以通过这个命令来执行。
之前说过Task与Task之间是存在依赖关系的。也就是说我们可以自己添加一些定制化的Task。 比如这个assemble任务添加一个SpecialTest任务。
Gradle工作流程
Gradle 工作包含三个阶段:
- 首先是初始化,对于我们的项目来说就是执行setting.gradle
- 然后是configuration阶段,这个阶段会解析你每个project中的build.gradle。这两个阶段之间我们可以加入一些定制化的HOOK(钩子),{截获消息,进行一些预处理,}
- 最后是执行任务阶段。也可以加入Hook执行一些操作。
https://docs.gradle.org/current/dsl/。。
Gradle对象与属性
Gradle主要有三种对象,这三种对象和三种不同的脚本文件对应。
- Gradle对象,当我们执行Gradle xxx的时候,gradle会从默认的配置脚本中构造出一个Gradle对象。整个执行过程中,就只有这么一个对象。
- Project 对象: 每一个build.gradle会转换成一个Project对象。
- Settings对象:每一个settings.gradle都会转换成一个Settings对象
先来看Gradle对象的属性
以这三个属性为例,写了个小demo
我在setting.gradle和 根项目下的bulid.gradle中分别加入了如下代码:
//在 settings.gradle 中,则输出"In settings,gradle id is"
println "In posdevice, gradle id is " + gradle.hashCode()
println "Home Dir:" + gradle.gradleHomeDir
println "User Home Dir:" + gradle.gradleUserHomeDir
println "Parent: " + gradle.parent
运行结果:
我们可以看到gradle的hashCode是一样的,说明只有一个gradle对象。
HomeDir :gradle执行程序的目录
User Home Dir: 是gradle自己设置的目录,里边包含一些配置文件,以及缓存文件,编译依赖的插件等等。
Project对象
之前说过一个Project包含若干Tasks。其实,一个Project包含多少Task一般都是由插件决定的。
加入插件的语句就是这个
Gralde插件能够:
- 在项目中添加新任务
- 为新加入的项目提供默认配置,这个默认配置会在项目中注入新的约定(如原文件位置)。
- 加入新的属性,可以覆盖插件的默认配置属性。
- 为项目加入新的依赖。
可以说Gradle的特性都是由插件提供的。这也是gradle最核心的思想插件化。
android plugin 最核心的四个task:
assemble 构建项目输出 check 运行检测和测试任务 build 运行assemble和check clean 清算输出任务
履行任务可以通过gradle/gradlew+任务名称的方式执,履行1个顶级任务会同时履行与其依赖的任务,比如你履行
gradlew assemble
它通常会履行:
gradlew assembleDebug gradlew assembleRelease
这时候会在你项目的build/outputs/apk或build/outputs/aar目录生成输出文件
同时,我们也可以为Project对象设置一些属性。也就是我们之前说到的gradle.properties文件。这个是项目中的gradle.properties文件。
它设置了APK的签名以及签名库。 而这个以systemProp为前缀表示的是System属性。 org.daemon 和 parallel主要是为了优化、加快Gradle构建而设置的属性。
除了使用gradle.properties文件为Project对象添加属性外,还可以在build.gradle文件中使用ext前缀来标示一个额外的属性。
运行结果:
说了这么多概念,现在应该重点说一说,这四个build.gradle了。其实我们构建大部分都是在玩这几个东西。我们一个一个看
HaodfPlus/build.gradle
这些buildscript{}、repositories {} 、dependencies{}..其实在Gradle中被称为Script Block。Script Block作用就是让我们
配置相关的信息。他们也就是我之前说过的闭包,在Gradle中大量的使用这种方式来配置Gradle。
比较奇怪的是为什么会有两个依赖?在buidscript中的依赖是gradle脚本的依赖。而在allprojects中的依赖是整个项目的依赖。
buildscript代码块中的repositories和dependencies的使用方式与直接在build.gradle文件中的使用方式几乎完全一样。唯一不同之处是在buildscript代码块中你可以对dependencies使用classpath声明。该classpath声明说明了在执行其余的build脚本时,class loader可以使用这些你提供的依赖项。这也正是我们使用buildscript代码块的目的。
App/build.gradle
默认情况下, 已经有了一个 debug 的签名配置,它使用了debug keystore,该keystore有一个已知的密码和默认的带有已知密码的key。
关于android的gradle配置在官方的文档上有详细的说明,地址:https://developer.android.com/tools/building/configuring-gradle.html
dependencies
- compile:编译项目代码所需要的依赖。
- runtime:运行时所需要的依赖。默认情况下,包含了编译时期的依赖。
- testCompile:编译测试代码时所需要的依赖。默认情况下,包含了编译时产生的类文件,以及编译时期所需要的依赖。
- testRuntime:测试运行时期的依赖。默认情况下,包含了上面三个时期的依赖。
- releaseCompile :编译realease版本所需要的依赖。
- debugCompile :编译debug版本所需要的依赖。
这些都是android 默认的依赖方法。 通过Build Variants(构建变种版本)还可以生成不同的构建依赖。
Build Variants(构建变种版本)
这也是gradle构建最强大的功能。
新构建系统的目标之一就是为同一个应用创建不同的版本。
主要有两个使用场景:
同一个应用的不同版本。比如一个免费的版本和一个付费的专业版本。
同一个应用被打包成多个不同的apk以发布各个市场商店 (渠道包)
综合第1条和第2条。
我们的目标就是基于同一个工程生成不同的APK,而不是使用一个单独的库工程和两个以上的应用工程组合生成APK的方式。
Gradle 实例应用
渠道包
每次发新版本时,我们的App会需要发布到各个应用市场,如下图:
为了统计各个市场的效果(下载量、活跃数),需要针对每一个渠道单独打包。如果让你打几十个市场的包岂不烦死了,不过有了Gradle,这再也不是事了。
以友盟统计为例。
在manifest.xml中有这样一段代码:
里面的CHANNEL_VALUE就是我们渠道的唯一标识。我们的目标就是在编译的时候这个值能够自动变化。
第一步、manifest中配置上面的代码
第二步、在build.gradle设置productFlavors
所谓ProductFlavors其实就是可定义的产品特性,配合
manifest
使用的时候就可以达成在一次编译过程中产生多个具有自己特性配置的版本。
这种被设计的新概念对于版本间差异非常小的时候很有用。
命名必须是唯一的标识,能够让gradle区分
这个是批量设置,也可以单独设置
最后,直接执行./gradlew assembleRelease,然后你就可以去喝个咖啡什么的等打包完成。
除此之外 assemble 还能和 Product Flavor 结合创建新的任务,其实 assemble 是和 Build Variants 一起结合使用的,而 Build Variants = Build Type + Product Flavor , 举个例子大家就明白了:
如果我们想打包wandoujia渠道的release版本,执行如下命令就好了:
./gradlew assembleWandoujiaRelease
如果我们只打wandoujia渠道版本,则:
./gradlew assembleWandoujia
此命令会生成wandoujia渠道的Release和Debug版本
这个是当渠道不多的时候可以采用的方法。当渠道过多的时候,可以考虑采用美团的方案(使用Python打渠道包)。
渠道包适配
有时候渠道多了,不同渠道对应的要求也就不同。而通过Gradle flavor,就可以很好的解决这个问题。
案例
使用不同的包名
有时候一个APP可能会存在手机版和HD版,两个版本代码不一样,目前HD版对应的代码不在维护,希望直接使用手机版的代码。
解决方案有很多,但是使用flavor解决就比较简单。
手机版的包名com.haodf.doctor
上面代码添加了一个hd的flavor,并指定了包名,运行gradle assembleHd就能生成适配包
实现的原因:实际上productFlavors同defaultConfig具有相同的属性。然而当你在productFlavors中设置了同defaultConfig相同的属性后,
gradle在构建的时候后优先选择productFlavors中的配置,此时defaultConfig中的配置被覆盖掉了。
除此之外productFlavors还可以设置想混淆,签名等这样的属性,为每一个版本指定不同的配置。这就看你当前的需求是怎么样的。
控制是否自动更新
一般一个APP启动时会默认检查客户端是否有更新,如果有更新就会提示用户下载。但是有些渠道和应用市场不允许这种默认行为,所以在适配这些渠道时需要禁止自动更新功能。
解决的思路就是提供一个配置字段,该字段的值以决定是否开启自动更新功能。
Gradle会在generateSources阶段为flavor生成一个BuildConfig.java文件。BuildConfig类默认提供了一些常量字段,比如应用的版本名(VERSION_NAME),应用的包名(PACKAGE_NAME)等。更强大的是,开发者还可以添加自定义的一些字段。下面的示例假设wandoujia市场默认禁止自动更新功能:
使用不同应用名
不同的渠道可能需要不同的应用名,客户端经常会和一些应用分发市场合作,需要在应用的启动界面中加上第三方市场的Logo,类似这类适配形式还有很多。
Gradle在构建应用时,会优先使用flavor所属
dataSet
中的同名资源。所以,解决思路就是在flavor的dataSet
中添加同名的字符串资源,以覆盖默认的资源。下面以适配wandoujia
渠道的应用名为美团团购
为例进行介绍。除此之外,productFlavors还可以指定不同的维度,来实现多种版本的定制。
Flavor Dimensions:维度,用于创建出复合产品风味,这种产品风味是由多个风味维度组合出来的,例如:一个项目的发布的版本一方面可以从处理器架构来分为arm、x86,mips,另一方面又可以分为免费版和付费版,所以最终的产品风味肯定是这两个维度的组合,如arm免费版、arm付费版、x86 免费版、x86付费版、mips免费版,mips收费版。当你的产品风味很多的时候,比如大于3个维度,每个维度的取值还很多,就可以使用这种复合产品风格,来简化build脚本。注意,使用风味维度时,写法有点奇怪,是用逆向思维,申明维度之后,先写出维度的取值,再写出这个取值属于哪个维度,如:
android {
...
flavorDimensions "abi", "version" // 申明有两个维度:abi和version
productFlavors {
freeapp { // 维度的取值
flavorDimension "version" // 这个取值属于名为version的维度
...
}
x86 { //维度的取值
flavorDimension "abi" // 这个取值属于名为abi的维度
...
}
...
}
}
关于如何通过Gradle实现一套代码开发不同特性的APK: