tasks
下面的代码展示了三个Gradle task,稍后会讲解这三者的不同。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
task myTask {
println "Hello, World!"
}
task myTask {
doLast {
println "Hello, World!"
}
}
task myTask << {
println "Hello, World!"
}
|
我的目的是创建一个task,当它执行的时候会打印出来”Hello, World!”。当我第一次创建task的时候,我猜测应该是这样来写的:
1
2
3
|
task myTask {
println "Hello, World!"
}
|
现在,试着来执行这个myTask,在命令行输入gradle myTask,打印如下:
1
2
3
|
user$ gradle myTask
Hello, World!
:myTask UP-TO-DATE
|
这个task看起来起作用了。它打印了”Hello, World!”。
但是,它其实并没有像我们期望的那样。下面我们来看看为什么。在命令行输入gradle tasks来查看所有可用的tasks。
1
2
3
4
5
6
7
8
9
10
11
12
|
user$ gradle tasks
Hello, World!
:tasks
------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------
Build Setup tasks
-----------------
init - Initializes a new Gradle build. [incubating]
..........
|
等等,为什么”Hello, World!”打印出来了?我只是想看看有哪些可用的task,并没有执行任何自定义的task!
原因其实很简单,Gradle task在它的生命周期中有两个主要的阶段:配置阶段 和 执行阶段。
可能我的用词不是很精确,但这的确能帮助我理解tasks。
Gradle在执行task之前都要对task先进行配置。那么问题就来了,我怎么知道我的task中,哪些代码是在配置过程中执行的,哪些代码是在task执行的时候运行的?答案就是,在task的最顶层的代码就是配置代码,比如:
1
2
3
4
|
task myTask {
def name = "Pavel" //<-- 这行代码会在配置阶段执行
println "Hello, World!" ////<-- 这行代码也将在配置阶段执行
}
|
这就是为什么我执行gradle tasks的时候,会打印出来”Hello, World!”-因为配置代码被执行了。但这并不是我想要的效果,我想要”Hello, World!”仅仅在我显式的调用myTask的时候才打印出来。为了达到这个效果,最简单的方法就是就是使用Task#doLast()方法。
1
2
3
4
5
6
|
task myTask {
def text = 'Hello, World!' //configure my task
doLast {
println text //this is executed when my task is called
}
}
|
现在,”Hello, World!”仅仅会在我执行gradle myTask的时候打印出来。Cool,现在我已经知道如何配置以及使task做正确的事情。还有一个问题,最开始的例子中,第三个task的<<符号是什么意思?
1
2
3
|
task myTask2 << {
println "Hello, World!"
}
|
这其实只是doLast的一个语法糖版本。它和下面的写法效果是一样的:
1
2
3
4
5
|
task myTask {
doLast {
println 'Hello, World!' //this is executed when my task is called
}
}
|
但是,这种写法所有的代码都在执行部分,没有配置部分的代码,因此比较适合那些简小不需要配置的task。一旦你的task需要配置,那么还是要使用doLast的版本。
语法
Gradle脚本是使用Groovy语言来写的。Groovy的语法有点像Java,希望你能接受它。
如果你对Groovy已经很熟悉了,可以跳过这部分了。
Groovy中有一个很重要的概念你必要要弄懂–Closure(闭包)
Closures
Closure是我们弄懂Gradle的关键。Closure是一段单独的代码块,它可以接收参数,返回值,也可以被赋值给变量。和Java中的Callable接口,Future类似,也像函数指针,你自己怎么方便理解都好。。。
关键是这块代码会在你调用的时候执行,而不是在创建的时候。看一个Closure的例子:
1
2
3
4
5
6
|
def myClosure = { println 'Hello world!' }
//execute our closure
myClosure()
#output: Hello world!
|
下面是一个接收参数的Closure:
1
2
3
4
5
6
|
def myClosure = {String str -> println str }
//execute our closure
myClosure( 'Hello world!' )
#output: Hello world!
|
如果Closure只接收一个参数,可以使用it来引用这个参数:
1
2
3
4
5
6
|
def myClosure = { println it }
//execute our closure
myClosure( 'Hello world!' )
#output: Hello world!
|
接收多个参数的Closure:
1
2
3
4
5
6
|
def myClosure = {String str, int num -> println "$str : $num" }
//execute our closure
myClosure( 'my string' , 21 )
#output: my string : 21
|
另外,参数的类型是可选的,上面的例子可以简写成这样:
1
2
3
4
5
6
|
def myClosure = {str, num -> println "$str : $num" }
//execute our closure
myClosure( 'my string' , 21 )
#output: my string : 21
|
很酷的是Closure中可以使用当前上下文中的变量。默认情况下,当前的上下文就是closure被创建时所在的类:
1
2
3
4
5
|
def myVar = 'Hello World!'
def myClosure = { println myVar}
myClosure()
#output: Hello world!
|
另外一个很酷的点是closure的上下文是可以改变的,通过Closure#setDelegate()。这个特性非常有用:
1
2
3
4
5
6
7
8
9
10
|
def myClosure = { println myVar} //I'm referencing myVar from MyClass class
MyClass m = new MyClass()
myClosure.setDelegate(m)
myClosure()
class MyClass {
def myVar = 'Hello from MyClass!'
}
#output: Hello from MyClass!
|
正如你锁看见的,在创建closure的时候,myVar并不存在。这并没有什么问题,因为当我们执行closure的时候,在closure的上下文中,myVar是存在的。这个例子中。因为我在执行closure之前改变了它的上下文为m,因此myVar是存在的。
把closure当做参数传递
closure的好处就是可以传递给不同的方法,这样可以帮助我们解耦执行逻辑。前面的例子中我已经展示了如何把closure传递给一个类的实例。下面我们将看一下各种接收closure作为参数的方法:
1.只接收一个参数,且参数是closure的方法: myMethod(myClosure)
2.如果方法只接收一个参数,括号可以省略: myMethod myClosure
3.可以使用内联的closure: myMethod {println ‘Hello World'}
4.接收两个参数的方法: myMethod(arg1, myClosure)
5.和4类似,单数closure是内联的: myMethod(arg1, { println ‘Hello World' })
6.如果最后一个参数是closure,它可以从小括号从拿出来: myMethod(arg1) { println ‘Hello World' }
这里我只想提醒你一下,3和6的写法是不是看起来很眼熟?
Gradle例子
现在我们已经了解了基本的语法了,那么如何在Gradle脚本中使用呢?先看下面的例子吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
}
}
allprojects {
repositories {
jcenter()
}
}
|
知道了Groovy的语法,是不是上面的例子就很好理解了?
首先就是一个buildscript方法,它接收一个closure:
1
|
def buildscript(Closure closure)
|
接着是allprojects方法,它也接收一个closure参数:
1
|
def allprojects(Closure closure)
|
其他的都类似。。。
现在看起来容易多了,但是还有一点不明白,那就是这些方法是在哪里定义的?答案就是Project
Project
这是理解Gradle脚本的一个关键。
构建脚本顶层的语句块都会被委托给Project的实例
这就说明Project正是我要找得地方。
在Project的文档页面搜索buildscript方法,会找到buildscript{} script block(脚本块).等等,script block是什么鬼?根据文档:
script block就是只接收closure作为参数的方法
继续阅读buildscript的文档,文档上说Delegates to: ScriptHandler from buildscript。也就是说,我们传递给buildscript方法的closure,最终执行的上下文是ScriptHandler。在上面的例子中,我们的传递给buildscript的closure调用了repositories(closure)和dependencies(closure)方法。既然closure被委托给了ScriptHandler,那么我们就去ScriptHandler中寻找dependencies方法。
找到了void dependencies(Closure configureClosure),根据文档,dependencies是用来配置脚本的依赖的。而dependencies最终又是委托到了DependencyHandler。
看到了Gradles是多么广泛的使用委托了吧。理解委托是很重要滴。
Script blocks
默认情况下,Project中预先定义了很多script block,但是Gradle插件允许我们自己定义新的script blocks!
这就意味着,如果你在build脚本顶层发了一些{…},但是你在Gradle的文档中却找不到这个script blocks或者方法,绝大多情况下,这是一些来自插件中定义的script block。
android Script block
我们来看看默认的Android app/build.gradle文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
apply plugin: 'com.android.application'
android {
compileSdkVersion 22
buildToolsVersion "22.0.1"
defaultConfig {
applicationId "com.trickyandroid.testapp"
minSdkVersion 16
targetSdkVersion 22
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile( 'proguard-android.txt' ), 'proguard-rules.pro'
}
}
}
|
Task顺序
我注意到我在使用Gradle的时候遇到的大多数问题都是和task的执行顺序有关的。很明显如果我的构建会工作的更好如果我的task都是在正确的时候执行。下面我们就深入了解一下如何更改task的执行顺序。
dependsOn
我认为最直接的方式来说明的你task的执行时依赖别的task的方法就是使用dependsOn方法。
比如下面的场景,已经存在task A,我们要添加一个task B,它的执行必须要在A执行完之后:
这是一个很简单的场景,假定A和B的定义如下:
1
2
|
task A << {println 'Hello from A' }
task B << {println 'Hello from B' }
|
只需要简单的调用B.dependsOn A,就可以了。
这意味着,只要我执行task B,task A都会先执行。
1
2
3
4
5
|
paveldudka$ gradle B
:A
Hello from A
:B
Hello from B
|
另外,你也可以在task的配置区中来声明它的依赖:
1
2
3
4
5
6
7
|
task A << {println 'Hello from A' }
task B {
dependsOn A
doLast {
println 'Hello from B'
}
}
|
如果我们想要在已经存在的task依赖中插入我们的task该怎么做呢?
过程和刚才类似。假定已经存在如下的task依赖:
1
2
3
4
5
6
|
task A << {println 'Hello from A' }
task B << {println 'Hello from B' }
task C << {println 'Hello from C' }
B.dependsOn A
C.dependsOn B
|
加入我们的新的task
1
2
3
|
task B1 << {println 'Hello from B1' }
B1.dependsOn B
C.dependsOn B1
|
输出:
1
2
3
4
5
6
7
8
9
|
paveldudka$ gradle C
:A
Hello from A
:B
Hello from B
:B1
Hello from B1
:C
Hello from C
|
注意dependsOn把task添加到依赖的集合中,所以依赖多个task是没有问题的。
1
2
3
|
task B1 << {println 'Hello from B1' }
B1.dependsOn B
B1.dependsOn Q
|
输出:
1
2
3
4
5
6
7
8
9
|
paveldudka$ gradle B1
:A
Hello from A
:B
Hello from B
:Q
Hello from Q
:B1
Hello from B1
|
mustRunAfter
现在假定我又一个task,它依赖于其他两个task。这里我使用一个真实的场景,我有两个task,一个单元测试的task,一个是UI测试的task。另外还有一个task是跑所有的测试的,它依赖于前面的两个task。
1
2
3
4
5
6
|
task unit << {println 'Hello from unit tests' }
task ui << {println 'Hello from UI tests' }
task tests << {println 'Hello from all tests!' }
tests.dependsOn unit
tests.dependsOn ui
|
输出:
1
2
3
4
5
6
7
|
paveldudka$ gradle tests
:ui
Hello from UI tests
:unit
Hello from unit tests
:tests
Hello from all tests!
|
尽管unitest和UI test会子啊test task之前执行,但是unit和ui这两个task的执行顺序是不能保证的。虽然现在来看是按照字母表的顺序执行,但这是依赖于Gradle的实现的,你的代码中绝对不能依赖这种顺序。
由于UI测试时间远比unit test时间长,因此我希望unit test先执行。一个解决办法就是让ui task依赖于unit task。
1
2
3
4
5
6
7
|
task unit << {println 'Hello from unit tests' }
task ui << {println 'Hello from UI tests' }
task tests << {println 'Hello from all tests!' }
tests.dependsOn unit
tests.dependsOn ui
ui.dependsOn unit // <-- I added this dependency
|
输出:
1
2
3
4
5
6
7
|
paveldudka$ gradle tests
:unit
Hello from unit tests
:ui
Hello from UI tests
:tests
Hello from all tests!
|
现在unit test会在ui test之前执行了。
但是这里有个很恶心的问题,我的ui测试其实并不依赖于unit test。我希望能够单独的执行ui test,但是这里每次我执行ui test,都会先执行unit test。
这里就要用到mustRunAfter了。mustRunAfter并不会添加依赖,它只是告诉Gradle执行的优先级如果两个task同时存在。比如我们这里就可以指定ui.mustRunAfter unit,这样如果ui task和unit task同时存在,Gradle会先执行unit test,而如果只执行gradle ui,并不会去执行unit task。
1
2
3
4
5
6
7
|
task unit << {println 'Hello from unit tests' }
task ui << {println 'Hello from UI tests' }
task tests << {println 'Hello from all tests!' }
tests.dependsOn unit
tests.dependsOn ui
ui.mustRunAfter unit
|
输出:
1
2
3
4
5
6
7
|
paveldudka$ gradle tests
:unit
Hello from unit tests
:ui
Hello from UI tests
:tests
Hello from all tests!
|
依赖关系如下图:
mustRunAfter在Gradle2.4中目前还是实验性的功能。
finalizedBy
现在我们已经有两个task,unit和ui,假定这两个task都会输出测试报告,现在我想把这两个测试报告合并成一个:
1
2
3
4
5
6
7
8
9
10
|
task unit << {println 'Hello from unit tests' }
task ui << {println 'Hello from UI tests' }
task tests << {println 'Hello from all tests!' }
task mergeReports << {println 'Merging test reports' }
tests.dependsOn unit
tests.dependsOn ui
ui.mustRunAfter unit
mergeReports.dependsOn tests
|
现在如果我想获得ui和unit的测试报告,执行task mergeReports就可以了。
1
2
3
4
5
6
7
8
9
|
paveldudka$ gradle mergeReports
:unit
Hello from unit tests
:ui
Hello from UI tests
:tests
Hello from all tests!
:mergeReports
Merging test reports
|
这个task是能工作,但是看起来好笨啊。mergeReports从用户的角度来看感觉不是特别好。我希望执行tests task就可以获得测试报告,而不必知道mergeReports的存在。当然我可以把merge的逻辑挪到tests task中,但我不想把tests task搞的太臃肿,我还是继续把merge的逻辑放在mergeReports task中。
finalizeBy来救场了。顾名思义,finalizeBy就是在task执行完之后要执行的task。修改我们的脚本如下:
1
2
3
4
5
6
7
8
9
10
11
|
task unit << {println 'Hello from unit tests' }
task ui << {println 'Hello from UI tests' }
task tests << {println 'Hello from all tests!' }
task mergeReports << {println 'Merging test reports' }
tests.dependsOn unit
tests.dependsOn ui
ui.mustRunAfter unit
mergeReports.dependsOn tests
tests.finalizedBy mergeReports
|
现在执行tests task就可以拿到测试报告了:
1
2
3
4
5
6
7
8
9
|
paveldudka$ gradle tests
:unit
Hello from unit tests
:ui
Hello from UI tests
:tests
Hello from all tests!
:mergeReports
Merging test reports
|