自定义Gradle tasks可以显著提高一个开发者的日常生活。任务可以操作存在的构建过程,添加新的构建步骤,或影响构建输出。你可以运行简单的任务,例如通过hooking到Gradle的Android插件,给一个生成的APK重命名。任务也能更加复杂的代码,你就可以在构建过程任何细节上做修改。当你了解如何hook到Android插件之后,更是如此。
定义任务
任务属于一个Project对象,并且每个任务都可以执行task接口。定义一个新任务的最简单的方式是,执行将任务名称作为其参数的任务方法:
task hello
其创建了任务,但当你执行时,它不会做任何事情。为了常见一个有用的任务,你需要添加一些动作,初学者通常会犯的一个错误像下面这样创建任务:
task hello{
println ‘hello,world!’
}
当你执行该任务时,会看到如下输出
hello,world!
从输出来看,你可能觉得任务运行了,但实际上,hello,world!在执行该任务之前就已被打印出来了。为了理解发生了什么,我们需要回到最初,我们知道在任意Gradle构建中,都有三个阶段:初始化阶段,配置阶段和执行阶段。当像上个例子那样以相同的方式添加代码到一个任务时,你实际上设置了任务的配置。即使你执行了不同的任务,hello,world!也依然会出现。
如果你想在执行阶段给一个任务添加动作,则可以使用下面的表示法:
task hello << {
println ‘hello,world!’
}
唯一的不同就是closure之前的<<,其告知Gradle,代码在执行阶段执行,而不是在配置阶段。
Groovy有很多简写,在Gradle中定义任务的常用方式有以下几种:
task(hello) << {
println ‘hello,world!’
}
task(‘hello’) << {
println ‘hello,world!’
}
tasks.create(name:‘hello’) << {
println ‘hello,world!’
}
前两个代码块只是以两种不同的方式通过Groovy来实现相同的事情。你可以使用括号,但你不需要这么做。你也不需要给参数加上单引号。在这两个代码块中,我们可以调用task()方法,其需要两个参数:一个是名为任务的字符串,另一个是closure。task()方法是Gradle Project类的一部分。
最后一个代码块没有使用task()方法。相反用了一个名叫tasks的对象。tasks对象是TaskContainer的实例,存在于每个Project对象中。该类提供了一个create()方法,需要一个Map和一个closure作为参数,最终返回一个Task。
任务剖析
Task接口是所有任务的基础,其定义了一系列属性和方法。所有这些都是由一个叫做DefaultTask的类实现的。这个标准的任务实现方式,你创建的每个新的任务,都是基于DefaultTask的。
每个任务都包含一个Action对象的集合。当一个任务被执行时,所有这些动作会以连续顺序被执行。你可以使用doFirst()和doLast()方法来为一个任务添加动作。这些方法都是以一个closure作为参数,然后被包装到一个Action对象中的。
如果你想在执行阶段执行你的代码,那么你需要使用doFirst()或doLast()来为一个task添加代码。我们之前用来定义tasks的左位移运算符(<<),就是doFirst()方法的简写。
下面使用doFirsh和doLast来写:
task hello{
println “Configuration”
doLast{
println "doLast"
}
doFirst{
println "doFirst"
}
}
执行结果
即使打印“doLast”的代码在打印“daFirst”的代码之前定义,但当执行task时,他们仍然会按正确的顺序执行。你甚至可以多次调用doFirst和doLast方法,如下所示:
task hello{
doFirst{
println "Not really first."
}
doFirst{
println "First"
}
doLast{
println "Not really last."
}
doLast{
println "Last."
}
}
执行这个task的输出如下:
注意,doFirst总是添加一个动作到task的最前面,而doLast总是添加一个动作到最后面。这意味着,当你使用这些方法时,需要小心,特别是当顺序十分重要的时候。
当涉及到tasks排序时,你可以使用mustRunAfter()方法。该方法将影响Gradle如何构建依赖关系图。当使用mustRunAfter()时,你需要指定,如果两个任务都被执行,那么必须有一个任务始终先执行。
task task1 << {
println 'task1'
}
task task2 << {
println 'task2'
}
task2.mustRunAfter task1
gradlew task2 task1
在这两个任务之间,mustRunAfter()方法不会添加任何依赖,因而我们可以只执行task2而不执行task1.如果你需要一个任务依赖另一个,那么可使用dependsOn()方法。
使用任务来简化release过程
在发布一个Android应用到应用商店之前,你需要使用证书对其签名。要想做到这一点,你需要创建自己的keystore,其中包含一对私钥。当你有了自己的keystore和私钥后,你就可以在FGradle中按照如下方式定义配置了:
signingConfigs {
release{
storeFile file("release.keystore")
storePassword "password"
keyAlias "ReleaseKey"
keyPassword "password"
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
这种方法的缺点是,你的keystore密码是以铭文的形式存放在依赖库中的,如果你正在为一个开源项目工作,那么这是明确禁止的。这会使得任意同时访问keystore文件和密码的人,都能使用你的身份发布应用程序。为了防止这种情况,你可以创建一个任务,在你每次装配发布包的时候,都询问发布密码。这样做有点麻烦,而且使得你不可能构建服务器来自动生成发布版本。有一个好的解决方案是创建一个配置文件,用来存放keystor密码,而不是将其包含到依赖仓库中。
在项目的根目录创建一个名为private。properties的文件夹,然后添加一行代码:
release.password = thepassword
我们假设keystore和密码本身相同。如果你有两个不同的密码,那么可以添加第二属性,这很容易。一旦设置完毕,你就可以定义一个名为getReleasePassword的新任务了:
task getReleasePassword << {
def password = ''
if (rootProject.file('private.properties').exists()){
Properties properties = new Properties();
properties.load(rootProject.file('private.properties').newDataInputStream())
password = properties.getProperty('release.password')
}
}
该任务会在项目的根目录搜索一个叫做private.properties的文件。如果该文件存在,则任务将加载其内容的所有属性。properties.load()方法梭巡key-value键值对,就像我们在属性文件中定义的release.password那样。
为了确保任何人在没有私有属性文件的情况下仍可运行脚本。可添加一个密码属性作为备用,以便运行脚本。
现在,我们的任务已经完成了,我们需要确保当执行release构建时,该任务被执行。要想做到这一点,需要在build.gradle文件中添加如下代码:
tasks.whenTaskAdded {
theTask ->
if (theTask.name.equals("packageRelease")) {
theTask.dependsOn "getReleasePassword"
}
}
这段代码通过添加一个closure来链接(hooks into)到Gradle和Android插件,其会在任务被添加到依赖关系图时运行。在packageRelease任务执行之前,不需要密码,所有我们可以确保packageRelease依赖于我们的getReleasePassword任务。我们不能只使用packageRealease.dependsOn()的原因时,Gradle的Android插件是基于构建variants动态生成的packaging任务,这意味着在Android插件发现所有构建variant之前,packageRelease任务都不会存在,即发现过程是在每个单独构建之前。
为了使用这个任务正常工作,hook到Gradle和Android插件是很有必要的。这是一个强大的概念。