我们在运行构建时,一定已经看到过任务旁边的 UP-TO-DATE 的提示,这就是增量构建的效果。
我们可以把一个 Task 的功能看做是处理输入素材的逻辑,它的产物是输出。如果一个 Task 的输入没有变,它的输出也一定不会变,而且这个 Task 之前应执行过一次,那么我们是不是只用检查输入就可以决定下一次是否需要再执行一遍这个 Task。
一个例子
task combineFileContentNonIncremental {
def sources = fileTree('srcFile')
def destination = file('destination.txt')
doLast {
destination.withPrintWriter { writer ->
sources.each {source ->
writer.println source.text
}
}
}
}
上面代码逻辑是,将所有 srcFile
目录下的文件的内容都拼接写到 destination.txt
文件中。这个 Task 没有使用增量构建,每次执行都会再执行一次逻辑。
下面我们将它改为支持增量构建:
task combineFileContentIncremental {
def sources = fileTree('srcFile')
def destination = file('destination.txt')
inputs.dir sources // 支持增量构建
outputs.file destination // 支持增量构建
doLast {
destination.withPrintWriter { writer ->
sources.each {source ->
writer.println source.text
}
}
}
}
根据文章开头增量构建的原理,要使 Gradle 支持增量构建,就需要开发者告诉 Gradle 自定义任务的输入和输出。如上面的代码,我们通过给 Task 的 inputs.dir
和 outputs.file
来告诉 Gradle 任务 combineFileContentIncremental
的输入是目录 srcFile
,输出是文件 destination.txt
。
自定义 Task 的增量构建
简单版
我们定义如下一个生成文件的 Generate
Task
public class Generate extends DefaultTask {
private int fileCount;
private String content;
private File generatedFileDir;
@Input
public int getFileCount() {
return fileCount;
}
public void setFileCount(int fileCount) {
this.fileCount = fileCount;
}
@Input
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@OutputDirectory
public File getGeneratedFileDir() {
return generatedFileDir;
}
public void setGeneratedFileDir(File generatedFileDir) {
this.generatedFileDir = generatedFileDir;
}
@TaskAction
public void perform() throws IOException {
for (int i = 1; i <= fileCount; i++) {
writeFile(new File(generatedFileDir, i + ".txt"), content);
}
}
private void writeFile(File destination, String content) throws IOException {
BufferedWriter output = null;
try {
output = new BufferedWriter(new FileWriter(destination));
output.write(content);
} finally {
if (output != null) {
output.close();
}
}
}
}
注意,我们使用了 @Input
和 @OutputDirectory
这两个注解,来告诉 Gradle 这个 Task 的输入和输出,这样 Gradle 第二次执行 Generate
时,就可以判断是否可以不用再执行 Generate
,而是利用上一次执行的产物。Gradle 以此来支持增量构建。
复杂版
我们可以注意到上面的 Generate
Task 中有两个 @Input
,因此我们在使用时需要这样写:
tasks.register("generate", Generate) {
fileCount = 2
content = 'Hello World!'
generatedFileDir = file("$buildDir/generated-output")
}
注意到 fileCount
和 content
都是输入的配置信息,如果我们有更多的输入配置信息,那么我们可能会考虑将这些配置信息分类。这就涉及到 @Nested
注解。
新写一个类 ConfigData
来包装 Generate
中的两个 @Input
属性
public class ConfigData {
private int fileCount;
private String content;
public int getFileCount() {
return fileCount;
}
public void setFileCount(int fileCount) {
this.fileCount = fileCount;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
修改 Generate
类,将之前的 fileCount
和 content
的相关使用都删除,添加使用 ConfigData
:
public class Generate extends DefaultTask {
private File generatedFileDir;
// 由于下面的 configData 方法要使用,这里需要先构造出来
private ConfigData configData = new ConfigData();
// 添加方法接受一个 action,这里就是开发者使用 configData 使最终调用的地方
public void configData(Action<? super ConfigData> action) {
action.execute(configData);
}
@OutputDirectory
public File getGeneratedFileDir() {
return generatedFileDir;
}
public void setGeneratedFileDir(File generatedFileDir) {
this.generatedFileDir = generatedFileDir;
}
@TaskAction
public void perform() throws IOException {
for (int i = 1; i <= configData.getFileCount(); i++) {
writeFile(new File(generatedFileDir, i + ".txt"), configData.getContent());
}
}
private void writeFile(File destination, String content) throws IOException {
BufferedWriter output = null;
try {
output = new BufferedWriter(new FileWriter(destination));
output.write(content);
} finally {
if (output != null) {
output.close();
}
}
}
}
经过上面的修改,我们在使用时就可以如下使用:
tasks.register("generate", Generate) {
configData {
fileCount = 2
content = 'Hello World!'
}
// fileCount = 2
// content = 'Hello World!'
generatedFileDir = file("$buildDir/generated-output")
}
但是这里有个问题,当我们修改 fileCount = 3
后,再执行,发现是 up-to-date。这是因为上面代码我们没有告诉 Gradle 输入是什么(但是却通过 @OutputDirectory
告诉了输出是什么),这时 Gradle 在每次构建时都会认为是 up-to-date。
- 只告诉 Gradle 输入/输出中的一端,那么每次构建都是 up-to-date
- 输入/输出都没告诉,那么每次构建都会被 Gradle 重新执行
- 输入/输出都告诉,那么 Gradle 会执行增量构建
支持增量构建的复杂版
我们要使其能够增量构建,只需告诉 Gradle 任务 generate
的输入即可。
- 给
ConfigData
中fileCount
,content
的getter
方法都加上@Input
public class ConfigData {
private int fileCount;
private String content;
@Input
public int getFileCount() {
return fileCount;
}
public void setFileCount(int fileCount) {
this.fileCount = fileCount;
}
@Input
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
- 在
Generate
中使用@Nested
注解,来告诉 Gradle 任务的输入。添加如下语句即可
// 这里使用 Nested
@Nested
public ConfigData getConfigData() {
return configData;
}
更多内容可参考:https://docs.gradle.org/current/userguide/more_about_tasks.html
网友评论