备注:下方的*代表这章的重要性。
Gradle是新一代的自动化构建工具。如果读者熟悉Ant、 Maven则可以把Gradle理解为升级版的Ant或 Maven, Gradle可以完成Ant、Maven的所有工作,甚至整合Ant或Maven的功能。
本书之所以要介绍 Gradle,是因为 Android Studio所采用的构建工具就是 Gradle。
早期 Android采用Ant作为构建工具,后来才使用Gradle取代了Ant。与Ant、 Maven相比,Gradle的优势到底在哪里呢?归纳起来, Gradle的优势有以下几点:
- Ant、 Maven支持的构建操作, Gradle都可以支持
- Gradle提供了强大的依赖管理,完全支持已有的 Maven或Ivy仓库
- Gradle使用 Groovy语言来编写构建文件,构建文件的功能更加灵活。因此, Gradle的构建文件可支持高级API,允许开发人员对构建过程进行监视或配置管理。
- 使用领域对象模型来描述构建
- Gradle支持多项目构建
- 提供简单易用的自定义任务、自定义插件
下载和安装Gradle请按如下步骤进行:
①登录http://services.gradle.org/distributions/站点下载 Gradle最新版 ,下载 Gradle时有3个选择:源代码包(文件名包含src)、二进制文件包(文件名包含bin)和完整包(文件名包含all),通常建议下载完整包,该包内包含了 Gradle的源代码、二进制文件和文档。
②下载gradle-4.10.2-all.zip文件(这里看自己是什么版本的),将下载到的压缩文件解压缩到任意路径下,此处将其解压D:根路径下,解压缩后将看到如下文件结构
- bin:包含 Gradle的命令——gradle。
- docs:包含用户手册(包括PDF和HIML两种版本)、DSL参考指南、API文档
- lib:包含 Gradle核心,以及它依赖的JAR包
- media:主要包含 Gradle的一些图标
- sample:样例。
- src: Gradle源代码,仅供参考使用。
③ Gradle的运行需要JAVA_HOME环境变量,该环境变量应指向JDK安装路径。
④ Gradle工具的关键命令就是Gradle解压缩目录下bin路径下的gradle.bat,如果读者希望操作系统可以识别该命令,还应该将Gradle压缩目录下的bin路径添加到操作系统的PATH环境变量中。
提示:
当在命令行窗口、Shell窗口中输入一条命令后,操作系统会到PATH环境变量所指定的系列路径中去搜索,如果找到了该命令所对应的可执行程序,则运行该命令;否则,将提示找不到命令。如果读者不嫌麻烦,愿意每次都输入 D: \gradle-4.10.2\bin\gradle.bat的全路径来运行 Gradle,则可不将 Gradle解压缩目录下的bin路径添加到PATH环境变量中。
经过上面4个步骤,Gradle安装成功。读者可以启动命令行窗口,输入 gradle. bat 命令,如
看到如下提示信息,则表明 Gradle安装成功。
使用 Gradle非常简单,当正确安装 Gradle后,只要输入 gradle或 gradle.bat命令即可。
如果运行 gradle命令时没有指定任何参数, Grade会在当前目录下搜索build. gradle文件,如果找到了就以该文件作为构建文件,并执行指定任务。
要想让 Gradle使用其他构建文件,可以用 -- buildfile<构建文件>选项,其中 -- buildfile可以使 -b代替,这两个选项的作用完全一样。例如如下命令:
gradle -b a.xml //显式指定使用a.xml作为构建文件
gradle --buildfile b.xml //显式指定使用b.xml作为构建文件
如果希望 Gradle运行时只输出少量的必要信息,则可以使用--quiet或-q选项。
如果运行 Gradle时要显式指定希望运行的任务,则可以采用如下命令格式。
gradle [task1 [task2 [task3 ]...]]
实际上,如果读者需要获取 gradle命令的更多详细信息,则直接使用gradle -h选项(或--help选项)即可。
使用 Gradle的关键就是编写构建文件,构建文件的主要作用就是定义构建项目的各种任务(Task)和属性,每个任务可包含多个动作( Action), Gradle每次运行时可运行一个或多个任务。
Gradle构建文件的默认名字为 build.gradle,也可以取其他名字。但如果为该构建文件起其他名字、则意味者着要将这个文件名作为参数传给Gradle工具。可以将构建文件放在项目的任何位置,但通常做法是放在项目的根目录中,这样有利于保持项目的简洁和清晰。
下面是一个典型的 Gradle项目层次结构
://存放整要存放与测试相关的源文件和资源
┣━━src:分类存放各种源文件,资源文件
┃ ┣━━main:主要存放与 项目 相关的源文件和资源
┃ ┃ ┣━━java:存放与项目相关的Java源文件
┃ ┃ ┗━━resources:存放与项目相关的资源
┃ ┗━━test:主要存放与 测试 相关的源文件和资源
┃ ┣━━java:存放与测试相关的Java源文件
┃ ┗━━resources:存放与测试相关的资源
┣━━build:存放编译后的class文件、资源的文件夹,该文件夹与src具有对应关系
┣━━libs:存放第三方JAR包的文件夹
┗━━build.gradle: Gradle构建文件
如果使用 gradle命令构建过项目,那么在项目的根目录下会多出一个.gradle文件夹,在该文件夹中存放的是 Gradle 的构建信息,一般不要手动去修改、删除它。
Gradle构建文件本质上是一个 Groovy源文件,因此该文件的语法完全符合 Groovy语法一一不过读者不用担心,本节不打算详细介绍 Groovy编程,只是简单介绍 Gradle构建文件的语法。
Gradle采用领域对象模型的概念来组织构建文件,在整个构建文件中涉及如下最核心的API
- Project:代表项目,通常一份构建文件代表一个项目。 Project包含大量属性和方法。
- TaskContainer:任务容器。每个 Projcct 都会维护一个 TaskContainer类型的 tasks属性。简而言之, Project和 TaskContainer有一一对应的关系。
- Task:代表 Gradle要执行的一个任务。Task允许指定它依赖的任务、该任务的类型,也可通过 configure()方法配置任务。它还提供了 doFirst()、 doLast()方法来添加 Action。Action对象和 Closure对象都可代表 Action。
提示:
Closure代表一个闭包,所以 Action实际上就代表一个代码块。
从逻辑上看, Gradle构建文件具有如图1.3所示的结构。
为 Gradle构建文件创建Task有如下常用方法。
不管使用哪种方式来创建Task,通常都可为Task指定如下3个常用属性。
下面代码示范了创建Task最简单的方式。
//定义hello任务,传入的代码块负责配置该任务
task hello1{
println "配置第一个任务"
}
上面代码就是调用task方法定义了ー个名为“hello1”的任务,在创建hello1任务时传入了一个代码块,在该代码块内只包含一行简单的 println加语句。
接下来即可使用 gradle hello1命令来执行该任务,将可看到如下输出。
println语句并不是在运行阶段输出的,而是在配置阶段输出的。在创建Task时传入的代码块用于配置该Task。
此处需要对 Gradle的构建过程进行说明。Gradle是一种声明式的构建工具,使用Gradle构建时, Gradle并不是直接按顺序执行 build.gradle文件中的内容的, Gradle的构建过程可分为两个阶段。
- 第一阶段:配置阶段。在此阶段, Gradle会读取 build.gradle文件的全部内容来配置Project和Task,比如设置 Project和Task的 Property、处理Task之间的依赖关系等。
- 第二阶段:按依赖关系执行指定Task。
如果需要为Task添加 Action,则可通过Task的 doFirst、 doLast方法一一正如它们的名字所示的, doFirst用于将 Action添加在 Action序列的前面, doLast用于将 Action添加在 Action序后面。
如下代码示范了为Task添加 Action。
//定义hello2任务、传入的代码块负责配置该任务
task hello2 {
println "配置第二个任务"
//调用doLast方法为Task添加 Action
doLast{
//使用循环
for(i in 0..<5){
println i
}
}
//调用doFrist方法为Task添加 Action
doFrist{
//定文变量
def s = "fkjava.org"
//输出字符串模板
println "开始执行第二个任务:$s"
}
}
上面代码同样调用task方法定义了一个hello2任务(包括hello1,程序清单一样),该任务的配置代码的第一行是println输出语句:接下来依次使用 doLast, doFirst方法添加了两个Action——正如大家所看到的,使用doLast, doFirst方法添加的 Action就是一个代码块,在这个代码块内完全可定义变量,也可使用循环等流程控制语句,只要这些语句符合 Groovy语法即可。这是多么强大的功能啊——Gradle构建文件不是简单的XML配置,而是完全支持Groovy编程语言,这就可以让 Gradle构建文件增加无限的可能性。
使用 gradle hello2命令来执行hello2任务,将可看到如下输出:
在配置阶段, Gradle会配置整个 Project和所有Task,因此可以看到hello1、hello2两个Task的配置代码的执行过程;在运行阶段,则只看到执行 hello2任务。从上面的执行结果可以看出,程序先执行 doFirst方法添加的 Action,再执行 doLast方法添加的 Action。
此外, Project对象还带一个 TaskContainer类型的tasks属性,因此也可在构建文件中通过 tasks属性的 create方法来创建Task。如下代码示范了使用 tasks属性的 create方法来创建Task(程序清单同上)。
//调用 Project的 tasks属性(Taskcontainer)的 create方法来创建Task
tasks.create(name:'showTasks'){
doLast{
//查看 Project的tasks属性的类型
println "tasks属性的类型:${tasks.class}"
//遍历 tasks属性
tasks.each{e ->
println e
}
}
}
上面代码使用 create方法创建了一个名为“ showTasks”的Task,使用 create方法传入的代码块用于配置该Task——该代码块只是使用 doLast为该Task添加了一个 Action,该 Action用于访问tasks属性的类型,并遍历该构建文件所包含的Task。
使用 gradle showTasks命令来执行 showTasks任务,将可看到如下输出。
在创建Task时可通过 dependsOn属性指定该Task所依赖的Task,也可通过type指定该Task的类型一一如果不指定type属性,Task的默认类型是Gradle实现的 DefaultTask类。
下面代码使用tasks属性的craete方法来创建Task,并指定 dependsOn和type属性(程序清单同上)。
//调用Project的tasks属性(TaskContalner)的create文件方法来创建Task
//dependsOn指定该Task依赖hello2,该Task的类型是Copy(拷贝文件)
tasks.create(name: 'fktask', dependsOn: 'he1lo2', type: Copy){
from 'books.xml'
into 'dist'
}
上面代码定义了一个名为“fktask”的Task,该Task依赖hello2,且该Task的类型为Copy(完成文件复制的Task,读者可通过 Gradle的API文档查看关于该任务的具体信息),该任务的from方法指定被复制的源文件,into方法指定复制的目标位置,该任务的默认 Action将会完成文件复制。
使用 gradle fkTask命令来执行 fkTask任务,将可看到如下输出。
从上面的执行过程可以看出,由于 flask任务依赖 hello2,因此在执行 flask任务之前会先执行hello2任务。上面命令执行完成后,可以看到项目根目录下的 books.xml文件被复制到dist目录下。
使用 Project的task方法创建Task时也可指定type、 dependsOn属性,例如如下代码定义了compile和run两个任务。由于这两个任务分别为 JavaCompile、 JavaExec类型,因此该构建文件要应用java插件。
构建代码如下(程序清单同上):
上面代码定义的 compile任务的类型是 JavaCompile(编译Java源程序的Task,读者可通过Gradle的API文档查看关于该Task的具体信息),在使用 JavaCompile时需要通过 source指定源代码所在路径,通过 destinationDir指定编译后的字节码文件的保存位置,该任务的默认 Action将会编译所有Java源文件。
上面run任务的类型是 JavaExec(运行Java程序的Task,读者可通过 Gradle的API文档查看关于该Task的具体信息),在使用JavaExec时需要通过main指定运行的主类。
为了看到Gradle编译、运行Java源代码的效果,应按Gradle的约定,将所有Java源文件放在src\main\java目录下,并创建build路径作为构建目录。
使用 gradle run命令来执行run任务,将可看到 Gradle先执行 compile 任务,然后再执run任务,构建过程生成如下输出。
除创建任务之外, Gradle构建文件的重要功能就是定义属性, Gradle允许为已Project和Task有的属性指定属性值,也可为Project和Task添加属性。下面依次介绍这几种情况。
Project、Task都是 Gradle提供的API,它们本身具有内置属性,因此可以在构建文件中为这些属性指定属性值。如下代码示范了为 Project、Task内置属性指定属性值。
//为 Project内置属性指定属性值
version =1.0
description= 'Project的属性'
//定义 showProps任务,显示 Project和里Task内置属性
task showProps{
//为Task内置属性指定属性值
description='Task的属性'
doLast{
println version
//输出Task属性
println description
//由于Task和 Project都有 description属性
//因此下面要显式指定访间 project的 description属性
printin project.description
}
}
不在任何Task之内,这就是为 Project 定义属性。第三行粗体字代码位于 showProps的初始化代码块内,因此表明为该任务定义属性。
使用 gradle showProps命令执行上面任务,可以看到如下输出
Project常用的属性有如下几个,这些属性大部分都可通过上面方式来指定属性值。
- name:项目的名字
- path:项目的绝对路径。
- description:项目的描述信息。
- buildDir:项目的构建结果的存放路径
- version:项目版本号
如果需要为 Project和Task添加属性,则可通过它们各自的ext进行添加。由于 Project和Task都实现了 Extensionaware接口,因此它们都可通过ext来添加属性。
提示:
Gradle中所有实现了 Extensionaware接口的API都可通过ext来添加属性。
如下代码示范了通过ext为Project、Task添加属性(程序清单同上)。
ext.prop1 = '添加的项目属性一'
ext.prop2 = '添加的项目属性二'
//使用ext方法,传入代码块来设置属性
ext{
prop3 = '添加的项目属性三'
prop4 = '添加的项目属性四'
}
task showAddedProps{
ext.prop1 = '添加的任务属性一'
ext.prop2 = '添加的任务属性二'
//使用ext方法,传入代码块来设置属性
ext{
prop3 = '添加的任务属性三'
prop4 = '添加的任务属性四'
}
doLsat{
println prop1
println project.prop1
println prop2
println project.prop2
println prop3
println project.prop3
println prop4
println project.prop4
}
}
两种方式:1.通过ext添加属性 2.通过ext方法传入代码块添加属性。
Gradle还允许在运行gadle命令时通过-P选项来添加属性,使用这种方式添加的属性都是项目属性。
task showCmdProp{
doLast{
println("系统显卡类型:${graphics}")
println("系统显卡类型:${project.graphics}")
}
}
上面代码在 showCmdProp任务中访问了项目的 graphics属性(加不加 project的效果相同),如果直接运行该任务,将会出现异常,因为 graphics属性不存在。
可以通过 gradle -P graphics= ATI showCmdProp命令来添加属性,它通过-P选项指定了graphics属性,该属性值为ATI。运行上面命令,可以看到如下输出。
Gradle也允许在运行 gradle命令时通过JVM参数来添加属性,Java允许通过-D选项为JVM设置参数,使用这种方式添加的属性也都是项目属性。例如定义如下任务(程序清单同上)
task showJVMProp{
doLast{
println("添加的JVM属性:${p1}")
}
}
上面代码在 showJVMProp任务中访间了项目的p1属性(加不加 project. 的效果相同),如果直接运行该任务,将会出现异常,因为p1属性不存在。
可以通过 gradle -D org.gradle.project.p1= sss showJVMProp命令来添加属性,它通过-D选项指定了p1属性,该属性值为sss。运行上面命令,可以看到如下输出。
从上面运行的命令可以看出,在通过-D选项来添加属性时,每个属性都需要以 "org.gradle.project"为前缀。
此外, Gradle还允许通过环境变量来添加属性(第五种方法)。由于这种方式比较少见,故此处不再给出详细介绍和示例。
当使用 Gradle构建一个任务时,如果该任务是一个非常耗时的任务,那么每次执行该任务时都需要耗费很长的时间——如果每次执行该任务时都没有造成任何改变,那么重复执行该任务就没有任何意义了。为此 Gradle引入了增量式构建的概念,如果任务的执行和前一次执行比较没有造成任何改变, Gradle不会重复执行该任务,这样就可以提高 Gradle的构建效率。
那么 Gradle如何判断任务的执行是否造成改变呢? Gradle将每个任务都当成一个黑盒子:只要该任务的输入部分没有改变,输出部分也没有改变一一 Gradle就会判断该任务的执行没有造成任改变。
Gradle的Task使用 inputs属性来代表任务的输入,该属性是一个 TaskInputs类型的对象;Task使用 outputs属性来代表任务的输出,该属性是一个 TaskOutputs类型的对象。
不管是 TaskInputs,还是 TaskOuputs,它们都支持设置文件、文件集、目录、属性等,只要它们没有发生改变, Gradle就认为该任务的输入、输出没有发生改变。
下面示例示范了 Gradle 的增量式构建。
//定义fileContentCopy任务
task fileContentCopy{
//定义代表 source目录的文件集
def sourceTxt = fileTree("source")
def dest = file('dist.txt')
//定义该任务的输入、输出
inputs.dir sourceTxt
outputs.file dest
doLast{
//调用File对象的 withPrintWriter方法
dest.withPrintWriter{ writer ->
//调用 sourceTxt的each方法遍历每个文件
sourceTxt.each{
writer.write(s.text)
}
}
}
}
上面任务的输入输出代码就是该构建文件的关键代码,其中 inputs.dir sourceTxt指定该任务的输入目录是 sourceTxt,只要该目录没有发生改变、目录里的内容没有发生改变, Gradle就认为该任务的的输入部分没有改变;outputs.file dest指定该任务的输出文件是dest,只要该文件没有发生任何改变, Gradle就认为该任务的输出部分没有改变。
上面示例只是为 inputs指定了输入路径,其实同样还可指定输入文件、输入属性等,如果同时为 inputs指定多个输入成分,则只有当所有输入成分都没有发生改变时, Gradle才认为该任务的输入部分没有改变; outputs与此类似,可同时指定输出文件、输出目录、输出属性等。
第一次执行 gradle fileContentCopy命令,将看到如下输出。
从上面的输出结果可以看出,第二次执行该命令时不再真正执行该任务,程序只是将该任务更新到最新(up-to-date)。
如果删除上面构建文件中的两行代码,那么每次执行 gradle fileContentCopy命令时都会真正执行该任务,这意味着没有启用 Gradle的增量式构建。