最近工作中需要在android工程编译过程中加gradle task,拷贝文件。我在写task的过程中遇到了很多问题,于是在网上找到了这篇博客,里面总结了写gradle task时经常遇到的一些问题,我觉得总结得非常的好,所以翻译过来,希望能帮助到更多的开发者(ps:英文好的可以直接看英文的博客,我可能翻译的不是很好)。
前言
在Stack Overflow上活跃了大概5年的时间里,我发现尽管Gradle已经是一个被市场认可的成熟的编译工具,但是每天都有人在询问一些关于Gradle的基本的问题。这些问题大部分都涉及到task,task是Gradle里的一个基础的执行单元。以下这些是关于task的被问到的最多的问题:
- task自动执行?
- task没有执行?
- task没有按照预想的顺序执行?
- 找不到task?
- task总是提示已经是最新了(up-to-date)?
- task从来不提示已经是最新了(up-to-date)?
我在下面将逐一地解释这些问题。
task自动执行
我们先看一下下面这一段Gradle脚本:
println 'a'
task t1 {
doFirst {
println 'b'
}
}
println 'c'
task t2 << {
println 'd'
}
task t5 {
println 'e'
}
如果将这个脚本保存到build.gradle
并且用gradle
执行,你能猜到哪些行会在终端被打印出来吗?嗯,答案是a
,c
和e
。如果你猜对了,那么你知道其中的原理吗?
每一个Gradle执行过程都分为三个阶段:
- 初始化
在初始化阶段,Gradle会解析哪些项目(project)需要被构建,并且为这些项目分别创建一个Project
的实例。 - 配置
在配置阶段,上一个阶段创建的Project
对象会在这个阶段被配置,并且执行每一个与单个对象相关的构建脚本。任务和配置以及许多其他类型的对象会在这个阶段被创建和配置属性。 - 执行
在这个阶段,上一个阶段被创建的并且被当作参数从命令行传给gradle
的任务被执行。
在上面这个简单的例子中,其实是没有执行阶段的,因为没有具体的任务作为第三个参数传给gradle
。因此在这个例子中,所以的事情都只发生在配置阶段,所以打印b
和d
的语句被跳过了。值得注意的是,虽然e
会被打印,但是不代表t5
这个任务已经被执行了,它只是已经被配置好了。
task没有执行
我们接着看下面这个build.gradle
:
task itMustRun
如果我们执行命令gradle -i itMustRun
会发生什么呢?你可能想不到,Gradle会报告跳过了这个task。为什么会这样呢?因为这个task并没有配置任何具体的执行语句。每一个task都有一个列表,里面保存了当task被执行时需要被执行的语句。doFirst
和doLast
接口分别是用来在这个列表的的首部和尾部加入执行语句的。一个task是可以添加多个执行语句的,当然很少有人这样做。如果你使用Groovy来写Gradle脚本,<<
其实等同于doLast
。<<
已经被弃用,并且已经在计划移除它,然而还是有很多地方可以看到它。因此,itMustRun
应该像下面这样被配置:
task itMustRun {
doFirst {
println "It's running from start..."
}
doLast {
println "...to an end."
}
}
task没有按照预想的顺序执行
分析下面这个脚本(task已经被简化,省略了具体的执行内容,除check
本来就没有执行内容):
task unit(type: Test)
task functional(type: Test)
task check // lifecycle task
task report
我们假设这些task要按照下面这些规则被执行:
- 当
check
被执行时,unit
和functional
都应该被执行 unit
和functional
必须能分别被执行- 如果
unit
和functional
同时被执行,unit
应该总是先执行 report
应该总是在check
被执行之后执行
针对第一条规则,我们应该添加check dependsOn unit, functional
到脚本中。为什么不使用mustRunAfter
或者shouldRunAfter
呢?因为后面两个只定义了在任务们都被添加到graph中的情况下,task的执行顺序,而前者即使任务没有被传递到gradle也能强制执行任务。前者定义的是一种依赖关系。
第二和第三条规则只需添加functional mustRunAfter unit
就能满足。在这里我们使用了mustRunAfter
,因为我们想要两个任务能分别执行(没有依赖关系)并且在同时需要执行时能遵循特定的顺序执行。shouldRunAfter
应该也能满足要求,唯一的区别是shouldRunAfter
的约束性没有那么强。如果脚本里面定义的次序规则导致了一个循环或者在并行执行时除了shouldRunAfter
不被满足,其他条件都满足时,shouldRunAfter
的约束会失效。
想要在check
被执行后执行report
只需添加check finalizedBy report
。finalizedBy
同样定义了一种依赖关系只不过是在task被执行之后再执行而不是在task被执行之前执行。
以下是满足所有规则的脚本:
task unit(type: Test)
task functional(type: Test)
task check // lifecycle task
task report
check.dependsOn unit, functional
functional.mustRunAfter unit //shouldRunAfter
check.finalizedBy report
找不到task
在运行gradle tasks
命令之后,你看到了以下这些输出:
...
G tasks
-------
before
after
...
同时before
是先前一个插件添加的task。我们现在在before
和after
之间定义一个依赖:
task after {
group = 'g'
dependsOn before
doLast {
println 'after'
}
}
并且运行gradle after
命令,输出如下:
$ gradle after
...
> Could not get unknown property 'before' for task ':after' of type org.gradle.api.DefaultTask.
...
为什么会这样呢?gradle tasks
的输出已经清楚的显示两个task都已经定义了,但是当试图在两个task间添加一个依赖关系时却抛出了一个未知属性的异常。这是因为一些task是通过下面这种方式被添加进来的:
afterEvaluate {
task before {
group = 'g'
doLast {
println 'before'
}
}
}
因此只有当整个项目被配置好后task才被添加进来,才能被访问到。我们能做的是,将dependsOn before
改为dependsOn 'before'
,将对象替换成字符串就能正确的被解析了。另外一个方法是在whenTaskAdded
中配置依赖关系:
tasks.whenTaskAdded { t ->
println "Got task: $t"
//... further configuration
}
task总是最新的或者从来不是最新的
因为最后两点都涉及到相同的问题,所以放在一起分析。假设我们有这样一个task:
task createFile {
doLast {
file('whatever').createNewFile()
}
}
即使文件whatever
早已经存在了,当task被执行时,task的执行语句总是会被执行,文件会反复地被重新创建。如果你用-i
模式运行这个task,你会找到task总是被执行的原因——这个task没有设置任何输出(outputs)。我们添加一个输出再试试:
task createFile {
outputs.file('whatever')
doLast {
file('whatever').createNewFile()
}
}
现在你再和以前一样运行这个task时(确保文件whatever
不存在),task将会正确执行。在第一次运行时,task会执行,但是会输出与以前不同的消息,因为没有可用的缓存。如果你第二次运行它,它就不会运行了,因为它已经是最新状态了。编译工具的一项职责就是避免做重复无用的工作。从现在开始,除非文件whatever
被删除,createFile
将永远输出已经是最新的。这就是为什么你的task总是最新的或者从来都不是最新的(你的outputs配置错了)。但是硬币是有正反两面的,硬币的另一面,你也许已经猜到了,那就是inputs。inputs用来通知Gradle确实有一些东西已经被改变了,task应该被重新运行。我们添加一个input
试试:
task createFile {
inputs.file('data')
outputs.file('whatever')
doLast {
file('whatever').createNewFile()
}
}
如果再运行一次task,Gradle将会运行。再多运行一次,它会显示已经是最新的。一旦文件data
被改变了,task就会重新运行。简单来说,inputs和outputs的多少是Gradle缓存机制的基础。inpust和outputs是另外一个大的话题,值得另外写一篇文章来更好地描述其中的机制。
我希望这篇短的博客能帮助你们理解Gradle的task是如何工作的。如果你对于这篇博客提到的内容有任何的疑问,你可以联系我——Gradle的文档太烂了。
PS:我知道两年以前Gradle支持使用Kotlin,然而Groovy依然十分流行并且广泛的运用在Gradle脚本中——这是我在这篇博客中使用Groovy作为实例的原因。