每次编译项目的时候都是在命令行下对每个源文件执行编译命令,这种方式对于源文件不多的小项目还行,但是当项目比较大有成百上千个源文件需要编译时就比较痛苦了,所以才有了一些自动化构建工具的诞生
最早出现的构建工具是makefile,它主要用于C/C++项目,大家会发现Android的源码里面用的就是这一套构建机制。makefile文件将程序编译,链接,装载(编译原理上的东西,不熟悉的可以去翻阅相关书籍)的流程定义成一套统一的规则。其中就包括了:哪些源文件需要编译,如何去编译,依赖的库文件,以及如何生成最终的可执行文件等等。通过这些规则去实现我们的构建需求,那么当你在编译整个工程的时候就只需要在命令行下执行一个make命令就可以搞定了,极大的提高了项目的构建效率。
ant也是一套构建工具,主要应用于Java项目(在eclipse上开发Android项目的时候用的比较多)。它是一个将软件编译,测试,部署过程组织起来自动化执行的工具。
ant虽然能大幅提高构建的效率,但是也存在一些缺点,比方说ant中的组件依赖(jar包)不能跨网络使用,为了解决这个问题,于是maven出现了,maven使用了强大的中央仓库,使得项目中使用到的一些公共组件可以很方便的联网依赖和更新,这也极大的方便了一些开源项目的使用。
由于maven的配置过于复杂和繁琐,于是出现了我们今天的主角gradle,下面给大家看下两者配置文件的对比。
maven的配置:
<dependencies>
<dependency>
<groupId>com.crashlytics.sdk.androidgroupId>
<artifactId>crashlyticsartifactId>
<version>2.5.5version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
dependency>
dependencies>
gradle的配置
dependencies {
compile('com.crashlytics.sdk.android:crashlytics:2.5.5@aar')
testCompile('junit:junit:4.7')
}
Gradle 是一个基于Apache和Apache Maven概念的项目自动化构建工具。他使用一种基于Groovy的特定领域语言赖声明项目设置,而不是传统的XML。
构建叫Build也好,叫make也行。
在Android开发中,要想把开发的应用交给安装到手机使用,要经过下面几个步骤。
不仅仅是编写代码,而是把编写的代码打包成APK,然后对APK进行测试,然后上架到应用市场,发布到用户手中,其中打包生成APK安装包流程就是构建过程。
日常生活中,和构建最类似的一个场景就是做菜。输入各种食材,然后按固定的工序,最后得到一盘菜。当然,做同样一道菜,由于需求不同,做出来的东西也不尽相同。比如,宫保鸡丁这道菜,回民要求不能放大油、口淡的要求少放盐和各种油、辣不怕的男女汉子们可以要求多放辣子…总之,做菜包含固定的工序,但是对于不同条件或需求,需要做不同的处理。
一个完整的应用构建需要经理一下步骤:
在没有构建系统的时候,需要理解整个过程,知道每一步具体用到哪些工具和命令行,逐个并调用,并把每一步的构建产物传递给下一步,过程繁杂,每个开发人员,都必须了解,需要做的事情也大量重复。因此需要自动化构建工具。
再比如说,Android开发中,一个APP有多个版本,Release版、Debug版、Test版。甚至针对不同APP Store都有不同的版本。每次不同需求过来,都需要手动去搞,是不是特别麻烦。
gradle运行JVM上,因此需要安装JDK(1.8及以上)。JDK安装不再赘述。
下载 → 官网连接
新建一个Android项目,点击Run按钮直接就能运行。其实就是通过项目gradle目录,包含了gradle wrapper的配置文件,使用gradle wrapper的方式不需要提前将gradle下载好,而是会根据本地的缓存情况决定是否需要联网下载gradle。
android studio project目录下的gradlew和gradlew.bat这两个文件就是用来在命令行界面执行gradle命令的,其中gradlew是在Linux或Mac系统中使用的,gradlew.bat是在Windows系统中使用的。
Gradle选择了Groovy。Groovy基于Java并拓展了Java。 Java程序员可以无缝切换到使用Groovy开发程序。Groovy说白了就是把写Java程序变得像写脚本一样简单。写完就可以执行,Groovy内部会将其编译成Javaclass然后启动虚拟机来执行。当然,这些底层的渣活不需要你管。
除了可以用很灵活的语言来写构建规则外,Gradle另外一个特点就是它是一种DSL,即Domain Specific Language,领域相关语言。什么是DSL,说白了它是某个行业中的行话,行业内的人一说就知道什么意思。在Gradle中就是一些语句或者词语就代表那个意思,你如果学过Gradle一看就知道是什么,好像就是约定俗称的东西,比如sourceSets代表源文件的集合等,外,基于行话,我们甚至可以建立一个模板,使用者只要往这个模板里填必须要填的内容,Gradle就可以非常漂亮得完成工作,得到想要的东西。
所以Gradle的使用离不开下面两个基础知识:
参见:Gradle构建过程
Gradle中,每一个待编译的工程都叫一个Project。每一个Project在构建的时候都包含一系列的Task。比如一个Android APK的编译可能包含:Java源码编译Task、资源编译Task、JNI编译Task、lint检查Task、打包生成APK的Task、签名Task等。
一个Project到底包含多少个Task,其实是由编译脚本指定的插件决定。插件是什么呢?插件就是用来定义Task,并具体执行这些Task的东西。
Gradle它负责定义流程和规则。而具体的编译工作则是通过插件的方式来完成的。比如编译Java有Java插件,编译Groovy有Groovy插件,编译Android APP有Android APP插件,编译Android Library有Android Library插件
Task就是gradle执行的最小单元。Task,顾名思义就是任务,一个Task包含若干action,action是一个闭包。
使用task关键字创建Task
task myTask // myTask是新建Task的名字
task myTask { configure closure } // 正常写法 task + 名字 + 闭包
task myType << { task action } // <<符号是doLast的缩写 这种写法Gradle 5.0版本中已经移除
/*
Task创建的时候可以指定Type,通过type:名字表达。这是什么意思呢?其实就是告诉Gradle,
这个新建的Task对象会从哪个基类Task派生。比如,Gradle本身提供了一些通用的Task,
最常见的有Copy 任务。Copy是Gradle中的一个类。当我们:task myTask(type:Copy)的时候,
创建的Task就是一个Copy Task。
*/
task myTask(type: SomeType)
task myTask(type: SomeType) { configure closure }
Task一些知识点:
allprojects {
task hello
}
每个task中可以包含0到多个Action,这些Action保存在一个ArrayList成员变量中,当执行这个task时,会按照顺序依次执行所有的Action。
Task接口提供了两个相关的方法来声明task动作:doFirst(Closure)和doLast(Closure),当task被执行时,动作逻辑被定义为闭包参数被依次执行。
doFirst()方法会将Action添加到ArrayList的开头,而doLast()方法则将Action添加到ArrayList的末尾。可以有多个doFirst()和doLast(),配置task时会按照它们出现的先后顺序添加到Action列表中。
可以在创建task的同时添加Action,只需要将其添加到创建task的闭包中即可。
以下代码在创建ms1 task的时候为它添加了4个action。
task ms1 {
doFirst {
println("=================ms1 doFirst1=================")
}
doFirst {
println("=================ms1 doFirst2=================")
}
doLast {
println("=================ms1 doLast1=================")
}
doLast {
println("=================ms1 doLast2=================")
}
}
-- 执行这个task会产生如下输出 --
=================ms1 doFirst2=================
=================ms1 doFirst1=================
=================ms1 doLast1=================
=================ms1 doLast2=================
如下代码为build task增加了两个Action,如果其他没有对build task再做修改,那么这里添加的doFirst会作为build task的第一个Action最先被执行,而doLast则会作为build task的最后一个Action,在其他Action执行完成后再执行。
build {
doFirst {
println("=================build doFirst=================")
}
doLast {
println("=================build doLast=================")
}
}
如下代码为ms1 task添加了一个新的action,这个action被添加在了其他action的前面。
ms1.doFirst {
println("=================ms1 doFirst=================")
}
有些task是动态创建的,无法直接在build.gradle的最外层为其增加Action,又或者需要在task定义代码之前为其添加Action,这时可以将其放到Project.afterEvaluate()方法中去执行。
afterEvaluate {
assembleDebug {
doLast {
println("=================assembleDebug doLast=================")
}
}
}
或者可以在Task creation事件的回调中去添加。
tasks.whenTaskAdded { task ->
if (task.name == 'assembleDebug') {
task.doFirst {
println("=====================assembleDebug doFirst=========================")
}
}
}
添加action可以出现在代码任意地方,只要拿到对应的task就可以为其添加action。需要注意的是不能为正在运行的task添加action,否则会抛出异常。例如如下代码为myTask添加了一个action,在这个action执行的时候再为myTask添加另一个action,这时就会有运行时异常,但可以在一个task执行的时候为其他task添加action,这里将myTask改成任意其他的task名字是可以正常运行的。
myTask.doFirst {
println("=================ms1 doFirst=================")
myTask.doFirst {
println("=================add another action=================")
}
}
task提供了dependsOn,finalizedBy方法来管理task之间的依赖关系,依赖关系表达的是执行这个task时所需要依赖的其他task,也就是说这个task不能被单独执行,执行这个task之前或之后需要执行另外的task。
要在一个task之前执行另一个task,可以通过配置dependsOn的依赖关系实现。
例如有两个task:taskA和taskB,通过指定taskA.dependsOn taskB就可以让在执行taskA之前先执行taskB,或者指定taskB.dependsOn taskA就可以让在执行taskB之前先执行taskA。
task taskC{
doLast{
println "taskC"
}
}
task taskA{
doLast{
println "taskA"
}
}
// 方式一:在创建Task的时候,设置其依赖的task
//task taskB(dependsOn: [taskA,taskC]){
// println "taskB"
//}
task taskB{
doLast{
println "taskB"
}
}
//方式二:通过调用对象方法创建依赖关系
taskB.dependsOn(taskA,taskC))
要在一个task之后执行另一个task,无法通过配置dependsOn实现。要实现这个功能需要通过finalizedBy来实现。
同样的有两个task:taskA和taskB,通过指定taskA.finalizedBy taskB就可以让在执行taskA之后执行taskB,或者指定taskB.finalizedBy taskA就可以让在执行taskB之后先执行taskA。
// 任务B之后执行任务A和任务C
taskB.finalizedBy(taskA,taskC)
gradle在执行一组task的时候会根据他们的依赖关系生成一个task序列,然后按照序列的先后顺序来依次执行各个task。单个task的执行一定是一个原子过程。
在通过dependsOn和finalizedBy设定好task之间的依赖关系后,在执行一个task时就会根据依赖关系生成一个task队列。
例如,有四个task,task0,task1,task2,task3。指定依赖关系如下。
task0 dependsOn task1
task1 dependsOn task2
task0 finalizedBy task3
task task0{
doLast {
println "test0"
}
}
task task1{
doLast {
println "test1"
}
}
task task2{
doLast {
println "test2"
}
}
task task3{
doLast {
println "test3"
}
}
task0.dependsOn task1
task1.dependsOn task2
task0.finalizedBy task3
当执行gradlew task0时就会先生成 task2 -> task1 -> task0 -> task3这样一个序列,然后按照顺序来执行
在上述例子中,执行task0得到的tasks队列中每个task的位置都是明确的,它们的先后顺序也是完全确定的。然而更多的情况是,一个任务队列中有部分task的位置不是很明确。
例如,有四个task,task0,task1,task2,task3。指定依赖关系如下。
task0 dependsOn task1
task0 dependsOn task2
task0 finalizedBy task3
按照依赖关系,执行task0生成的任务队列可以是task2 -> task1 -> task0 -> task3,也可以是task1 -> task2 -> task0 -> task3,可以看到task2和task1之间的顺序是不确定的。虽然gradle总是可以按照一定的规则(例如task名字的字典序)来得到一个固定的task队列,但有时我们需要人为明确这个顺序,例如这里我们希望task2总是在task1之前执行。这时有两种方案,一种是使用dependsOn,我们让task1 dependsOn task2,这样task2一定会在task1之前执行,但这种方案会让单独执行task1的时候也会先执行task2,这有可能和实际需要不符。这时就需要使用另一种方案,通过shouldRunAfter和mustRunAfter来指定这个顺序。
task0.dependsOn task2
task0.dependsOn task1
task0.finalizedBy task3
task1.shouldRunAfter task2
shouldRunAfter和mustRunAfter表达的都是一个任务需要在另一个任务之后执行,它们的区别是mustRunAfter要求gradle生成任务队列时必须确保这个顺序一定要满足,如果不能满足就会报错,shouldRunAfter相当于建议gradle按照这顺序来生成任务队列,gradle会优先参考其他约束条件,如果在满足其他约束条件后,这条约束也能满足,那么就采纳这个顺序,如果不能满足,就忽略这个请求。
例如有依赖关系task0 dependsOn task1,这时又指定task1 mustRunAfter task0,显然这时这个条件无法被满足,因此执行task0的时候会报错。但如果指定task1 shouldRunAfter task0,则不会有错误,gradle会自动忽略掉这条请求。
再来看上述例子
task0 dependsOn task1
task0 dependsOn task2
task0 finalizedBy task3
这时无论指定task1 shouldRunAfter task2,还是task1 mustRunAfter task2,都会得到任务队列task2 -> task1 -> task0 -> task3。
需要注意的是,无论是shouldRunAfter还是mustRunAfter,影响的只是task在队列中的顺序,并不影响任何任务间的执行依赖,也就是说使用shouldRunAfter和mustRunAfter并不会导致任务队列中添加新的task。
例如指定ms1 mustRunAfter ms2,如果ms1和ms2没有其他依赖关系,那么在执行ms1的时候并不会先执行ms2,执行ms2之后也不会执行ms1
gradle为每个工程提供了很多预先定义好的task,通过这些task我们可以不需要手动创建任何task就可以实现编译,打包,测试等功能。经常使用的clean build等就是gradle中预定义的task。在做Android开发时,Android Gradle插件也提供了一组预定义的gradle task,当我们新建一个Android工程就能看到如下所示的task列表,这里的tasks都是gradle和Android Gradle Plugin为我们创建好的,可以直接使用。
clean task用来清理上次编译的缓存内容,会删除生成的build目录。
和编译相关的task主要有:build和assemble,其中build依赖assemble,也就是说执行build之前会先执行assemble。在Android上,会根据buildType和productFlavor的不同自动创建多个assembleXxx任务,如assembleDebug,assembleRelease等,assemble会依赖所有的assembleXxx任务,也就是说执行assemble会先执行assembleDebug,assembleRelease等一系列的assemble任务。
有时我们需要在编译任务完成后执行某一个任务,按照通常的做法,我们创建一个task,然后将这个task放到编译的task之后执行。
如前所述要在一个task之后执行另一个task,可以通过finalizedBy来实现,但是编译任务对应的task很多,有些还是动态创建的,如果每个任务都指定一遍finalizedBy会很麻烦。好在gradle提供了一个buidlFinished()方法,它可以在编译完成后执行一定的操作,不用理会当前执行的是哪个编译task,只需要把task中代码移到buidlFinished中即可。
project.gradle.buildFinished {
println "build finished"
}
要在当前任务里获取自己的名字,可以使用其name属性
task ms1{
doLast {
println name // ms1
}
}
可以在任务配置代码中,通过ext.xxx来为任务添加自定义属性。
task myTask {
ext.myProperty = "myValue"
}
Gradle 允许在build.gradle脚本中定义一个或多个默认任务,默认任务的意思是没有指定任何任务的时候会执行默认任务。要指定默认任务可以使用defaultTasks关键字。
例如,如下代码指定默认任务为myTask和build,则执行gradlew时如果没有指定任何任务,就会执行myTask和build。
defaultTasks 'myTask', 'build'
task myTask {
doLast {
println "run default task."
}
}
需要注意的是,使用defaultTasks指定默认任务的时候,任务名一定要加引号,即使它是在任务定义之后声明,也是如此。
有时我们可能需要覆盖掉一个已有的任务,这个任务可能是自定义的,或者是来自其他Gradle插件中的任务。虽然这种场景并不常见,不过还是有必要了解这种用法,说不定在需要的时候就能帮上大忙。
覆盖已有任务只需要重新创建一个新的同名task,并指定overwrite为true即可。
overwrite的覆盖操作,不仅会覆盖掉原先task的所有action,而且会覆盖原先task指定的type类型,还会覆盖掉所有和原先task相关的依赖关系。被覆盖的依赖关系不仅包含原先的task对其他task的依赖,还包含其他task对原先task的依赖,所有的和原先task相关的依赖关系都会被移除。当然,可以在overwrite之后重新定义新的依赖关系。
overwrite的覆盖操作不会覆盖原先创建task块中的代码,所有创建task代码仍然会被执行。
此外,如果定义overwrite的task在这之前并没有被创建,那么gradle会忽略这个属性,等同于创建一个新的task,不会有错误出现。
如下代码创建了两个名为copy的task,第二个copy task指定了overwrite为true,因此第二个copy task会覆盖第一个。此外,第一个copy task依赖一个名为delete的task,由于这条依赖关系在overwrite之前配置,所以同样会被覆盖。
task delete(type: Delete) {
delete "s1.txt"
}
task copy(type: Copy) {
println name
from(file('ss.txt'))
into(file('new_dir'))
doLast {
println "I am a copy task"
}
}
copy.dependsOn delete
task copy(overwrite: true) {
println name
doLast {
println('I am the new one.')
}
}
这里执行gradlew copy的完整流程如下。
在gradle世界中,是一个构建工具,让工程构建更自动化,不过,他只是一个运行环境,提供了基本的框架,而真正的构建行为不是它提供的,Gradle负责在运行的时候,找到所有需要执行的任务,挨个去执行,而任务是由谁来提供的呢?
gradle中几乎所有的功能,比如编译代码等都是以插件的形式去提供的。插件负责封装,并且提供gradle运行期间需要的task,在工程中依赖某个插件之后,就能复用插件提供的构建行为。
Gradle内置了很多的语言插件,基本上能够满足大部分的构建工作,但有些插件没有内置,或者某些功能没有提供,就可以通过自定义插件或者引入其他插件解决。比如Android gradle插件就是通过java插件去拓展的。它在编译Java代码的基础上,还添加了编译资源,打包APK等功能。
// 引入二进制插件
apply plugin: 'com.android.library' // <==如果是编译Library,则加载此插件
apply plugin: 'com.android.application' // <==如果是编译Android APP,则加载此插件
// 引入脚本插件
apply from: 脚本插件的绝对路径
apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"