簡(jiǎn)介
在我們使用的各種工具中,為了提升工作效率,總會(huì)使用到各種各樣的緩存技術(shù),比如說(shuō)docker中的layer就是緩存了之前構(gòu)建的image。在gradle中這種以task組合起來(lái)的構(gòu)建工具也不例外,在gradle中,這種技術(shù)叫做增量構(gòu)建。
增量構(gòu)建
gradle為了提升構(gòu)建的效率,提出了增量構(gòu)建的概念,為了實(shí)現(xiàn)增量構(gòu)建,gradle將每一個(gè)task都分成了三部分,分別是input輸入,任務(wù)本身和output輸出。下圖是一個(gè)典型的java編譯的task。
以上圖為例,input就是目標(biāo)jdk的版本,源代碼等,output就是編譯出來(lái)的class文件。
增量構(gòu)建的原理就是監(jiān)控input的變化,只有input發(fā)送變化了,才重新執(zhí)行task任務(wù),否則gradle認(rèn)為可以重用之前的執(zhí)行結(jié)果。
所以在編寫(xiě)gradle的task的時(shí)候,需要指定task的輸入和輸出。
并且要注意只有會(huì)對(duì)輸出結(jié)果產(chǎn)生變化的才能被稱(chēng)為輸入,如果你定義了對(duì)初始結(jié)果完全無(wú)關(guān)的變量作為輸入,則這些變量的變化會(huì)導(dǎo)致gradle重新執(zhí)行task,導(dǎo)致了不必要的性能的損耗。
還要注意不確定執(zhí)行結(jié)果的任務(wù),比如說(shuō)同樣的輸入可能會(huì)得到不同的輸出結(jié)果,那么這樣的任務(wù)將不能夠被配置為增量構(gòu)建任務(wù)。
自定義inputs和outputs
既然task中的input和output在增量編譯中這么重要,本章將會(huì)給大家講解一下怎么才能夠在task中定義input和output。
如果我們自定義一個(gè)task類(lèi)型,那么滿(mǎn)足下面兩點(diǎn)就可以使用上增量構(gòu)建了:
第一點(diǎn),需要為task中的inputs和outputs添加必要的getter方法。
第二點(diǎn),為getter方法添加對(duì)應(yīng)的注解。
gradle支持三種主要的inputs和outputs類(lèi)型:
簡(jiǎn)單類(lèi)型:簡(jiǎn)單類(lèi)型就是所有實(shí)現(xiàn)了Serializable接口的類(lèi)型,比如說(shuō)string和數(shù)字。
文件類(lèi)型:文件類(lèi)型就是 File 或者 FileCollection 的衍生類(lèi)型,或者其他可以作為參數(shù)傳遞給 Project.file(java.lang.Object) 和 Project.files(java.lang.Object...) 的類(lèi)型。
嵌套類(lèi)型:有些自定義類(lèi)型,本身不屬于前面的1,2兩種類(lèi)型,但是它內(nèi)部含有嵌套的inputs和outputs屬性,這樣的類(lèi)型叫做嵌套類(lèi)型。
接下來(lái),我們來(lái)舉個(gè)例子,假如我們有一個(gè)類(lèi)似于FreeMarker和Velocity這樣的模板引擎,負(fù)責(zé)將模板源文件,要傳遞的數(shù)據(jù)最后生成對(duì)應(yīng)的填充文件,我們考慮一下他的輸入和輸出是什么。
輸入:模板源文件,模型數(shù)據(jù)和模板引擎。
輸出:要輸出的文件。
如果我們要編寫(xiě)一個(gè)適用于模板轉(zhuǎn)換的task,我們可以這樣寫(xiě):
import java.io.File;
import java.util.HashMap;
import org.gradle.api.*;
import org.gradle.api.file.*;
import org.gradle.api.tasks.*;
public class ProcessTemplates extends DefaultTask {
private TemplateEngineType templateEngine;
private FileCollection sourceFiles;
private TemplateData templateData;
private File outputDir;
@Input
public TemplateEngineType getTemplateEngine() {
return this.templateEngine;
}
@InputFiles
public FileCollection getSourceFiles() {
return this.sourceFiles;
}
@Nested
public TemplateData getTemplateData() {
return this.templateData;
}
@OutputDirectory
public File getOutputDir() { return this.outputDir; }
// 上面四個(gè)屬性的setter方法
@TaskAction
public void processTemplates() {
// ...
}
}
上面的例子中,我們定義了4個(gè)屬性,分別是TemplateEngineType,F(xiàn)ileCollection,TemplateData和File。前面三個(gè)屬性是輸入,后面一個(gè)屬性是輸出。
除了getter和setter方法之外,我們還需要在getter方法中添加相應(yīng)的注釋?zhuān)?@Input , @InputFiles ,@Nested 和 @OutputDirectory, 除此之外,我們還定義了一個(gè) @TaskAction 表示這個(gè)task要做的工作。
TemplateEngineType表示的是模板引擎的類(lèi)型,比如FreeMarker或者Velocity等。我們也可以用String來(lái)表示模板引擎的名字。但是為了安全起見(jiàn),這里我們自定義了一個(gè)枚舉類(lèi)型,在枚舉類(lèi)型內(nèi)部我們可以安全的定義各種支持的模板引擎類(lèi)型。
因?yàn)閑num默認(rèn)是實(shí)現(xiàn)Serializable的,所以這里可以作為@Input使用。
sourceFiles使用的是FileCollection,表示的是一系列文件的集合,所以可以使用@InputFiles。
為什么TemplateData是@Nested類(lèi)型的呢?TemplateData表示的是我們要填充的數(shù)據(jù),我們看下它的實(shí)現(xiàn):
import java.util.HashMap;
import java.util.Map;
import org.gradle.api.tasks.Input;
public class TemplateData {
private String name;
private Map<String, String> variables;
public TemplateData(String name, Map<String, String> variables) {
this.name = name;
this.variables = new HashMap<>(variables);
}
@Input
public String getName() { return this.name; }
@Input
public Map<String, String> getVariables() {
return this.variables;
}
}
可以看到,雖然TemplateData本身不是File或者簡(jiǎn)單類(lèi)型,但是它內(nèi)部的屬性是簡(jiǎn)單類(lèi)型的,所以TemplateData本身可以看做是@Nested的。
outputDir表示的是一個(gè)輸出文件目錄,所以使用的是@OutputDirectory。
使用了這些注解之后,gradle在構(gòu)建的時(shí)候就會(huì)檢測(cè)和上一次構(gòu)建相比,這些屬性有沒(méi)有發(fā)送變化,如果沒(méi)有發(fā)送變化,那么gradle將會(huì)直接使用上一次構(gòu)建生成的緩存。
注意,上面的例子中我們使用了FileCollection作為輸入的文件集合,考慮一種情況,假如只有文件集合中的某一個(gè)文件發(fā)送變化,那么gradle是會(huì)重新構(gòu)建所有的文件,還是只重構(gòu)這個(gè)被修改的文件呢?
留給大家討論
除了上講到的4個(gè)注解之外,gradle還提供了其他的幾個(gè)有用的注解:
@InputFile: 相當(dāng)于File,表示單個(gè)input文件。
@InputDirectory: 相當(dāng)于File,表示單個(gè)input目錄。
@Classpath: 相當(dāng)于Iterable<File>,表示的是類(lèi)路徑上的文件,對(duì)于類(lèi)路徑上的文件需要考慮文件的順序。如果類(lèi)路徑上的文件是jar的話(huà),jar中的文件創(chuàng)建時(shí)間戳的修改,并不會(huì)影響input。
@CompileClasspath:相當(dāng)于Iterable<File>,表示的是類(lèi)路徑上的java文件,會(huì)忽略類(lèi)路徑上的非java文件。
@OutputFile: 相當(dāng)于File,表示輸出文件。
@OutputFiles: 相當(dāng)于Map<String, File> 或者 Iterable<File>,表示輸出文件。
@OutputDirectories: 相當(dāng)于Map<String, File> 或者 Iterable<File>,表示輸出文件。
@Destroys: 相當(dāng)于File 或者 Iterable<File>,表示這個(gè)task將會(huì)刪除的文件。
@LocalState: 相當(dāng)于File 或者 Iterable<File>,表示task的本地狀態(tài)。
@Console: 表示屬性不是input也不是output,但是會(huì)影響console的輸出。
@Internal: 內(nèi)部屬性,不是input也不是output。
@ReplacedBy: 屬性被其他的屬性替換了,不能算在input和output中。
@SkipWhenEmpty: 和@InputFiles 跟 @InputDirectory一起使用,如果相應(yīng)的文件或者目錄為空的話(huà),將會(huì)跳過(guò)task的執(zhí)行。
@Incremental: 和@InputFiles 跟 @InputDirectory一起使用,用來(lái)跟蹤文件的變化。
@Optional: 忽略屬性的驗(yàn)證。
@PathSensitive: 表示需要考慮paths中的哪一部分作為增量的依據(jù)。
運(yùn)行時(shí)API
自定義task當(dāng)然是一個(gè)非常好的辦法來(lái)使用增量構(gòu)建。但是自定義task類(lèi)型需要我們編寫(xiě)新的class文件。有沒(méi)有什么辦法可以不用修改task的源代碼,就可以使用增量構(gòu)建呢?
答案是使用Runtime API。
gradle提供了三個(gè)API,用來(lái)對(duì)input,output和Destroyables進(jìn)行獲?。?/p>
Task.getInputs() of type TaskInputs
Task.getOutputs() of type TaskOutputs
Task.getDestroyables() of type TaskDestroyables
獲取到input和output之后,我們就是可以其進(jìn)行操作了,我們看下怎么用runtime API來(lái)實(shí)現(xiàn)之前的自定義task:
task processTemplatesAdHoc {
inputs.property("engine", TemplateEngineType.FREEMARKER)
inputs.files(fileTree("src/templates"))
.withPropertyName("sourceFiles")
.withPathSensitivity(PathSensitivity.RELATIVE)
inputs.property("templateData.name", "docs")
inputs.property("templateData.variables", [year: 2013])
outputs.dir("$buildDir/genOutput2")
.withPropertyName("outputDir")
doLast {
// Process the templates here
}
}
上面例子中,inputs.property() 相當(dāng)于 @Input ,而outputs.dir() 相當(dāng)于@OutputDirectory。
Runtime API還可以和自定義類(lèi)型一起使用:
task processTemplatesWithExtraInputs(type: ProcessTemplates) {
// ...
inputs.file("src/headers/headers.txt")
.withPropertyName("headers")
.withPathSensitivity(PathSensitivity.NONE)
}
上面的例子為ProcessTemplates添加了一個(gè)input。
隱式依賴(lài)
除了直接使用dependsOn之外,我們還可以使用隱式依賴(lài):
task packageFiles(type: Zip) {
from processTemplates.outputs
}
上面的例子中,packageFiles 使用了from,隱式依賴(lài)了processTemplates的outputs。
gradle足夠智能,可以檢測(cè)到這種依賴(lài)關(guān)系。
上面的例子還可以簡(jiǎn)寫(xiě)為:
task packageFiles2(type: Zip) {
from processTemplates
}
我們看一個(gè)錯(cuò)誤的隱式依賴(lài)的例子:
plugins {
id 'java'
}
task badInstrumentClasses(type: Instrument) {
classFiles = fileTree(compileJava.destinationDir)
destinationDir = file("$buildDir/instrumented")
}
這個(gè)例子的本意是執(zhí)行compileJava任務(wù),然后將其輸出的destinationDir作為classFiles的值。
但是因?yàn)閒ileTree本身并不包含依賴(lài)關(guān)系,所以上面的執(zhí)行的結(jié)果并不會(huì)執(zhí)行compileJava任務(wù)。
我們可以這樣改寫(xiě):
task instrumentClasses(type: Instrument) {
classFiles = compileJava.outputs.files
destinationDir = file("$buildDir/instrumented")
}
或者使用layout:
task instrumentClasses2(type: Instrument) {
classFiles = layout.files(compileJava)
destinationDir = file("$buildDir/instrumented")
}
或者使用buildBy:
task instrumentClassesBuiltBy(type: Instrument) {
classFiles = fileTree(compileJava.destinationDir) {
builtBy compileJava
}
destinationDir = file("$buildDir/instrumented")
}
輸入校驗(yàn)
gradle會(huì)默認(rèn)對(duì)@InputFile ,@InputDirectory 和 @OutputDirectory 進(jìn)行參數(shù)校驗(yàn)。
如果你覺(jué)得這些參數(shù)是可選的,那么可以使用@Optional。
自定義緩存方法
上面的例子中,我們使用from來(lái)進(jìn)行增量構(gòu)建,但是from并沒(méi)有添加@InputFiles, 那么它的增量緩存是怎么實(shí)現(xiàn)的呢?
我們看一個(gè)例子:
public class ProcessTemplates extends DefaultTask {
// ...
private FileCollection sourceFiles = getProject().getLayout().files();
@SkipWhenEmpty
@InputFiles
@PathSensitive(PathSensitivity.NONE)
public FileCollection getSourceFiles() {
return this.sourceFiles;
}
public void sources(FileCollection sourceFiles) {
this.sourceFiles = this.sourceFiles.plus(sourceFiles);
}
// ...
}
上面的例子中,我們將sourceFiles定義為可緩存的input,然后又定義了一個(gè)sources方法,可以將新的文件加入到sourceFiles中,從而改變sourceFile input,也就達(dá)到了自定義修改input緩存的目的。
我們看下怎么使用:
task processTemplates(type: ProcessTemplates) {
templateEngine = TemplateEngineType.FREEMARKER
templateData = new TemplateData("test", [year: 2012])
outputDir = file("$buildDir/genOutput")
sources fileTree("src/templates")
}
我們還可以使用project.layout.files()將一個(gè)task的輸出作為輸入,可以這樣做:
public void sources(Task inputTask) {
this.sourceFiles = this.sourceFiles.plus(getProject().getLayout().files(inputTask));
}
這個(gè)方法傳入一個(gè)task,然后使用project.layout.files()將task的輸出作為輸入。
看下怎么使用:
task copyTemplates(type: Copy) {
into "$buildDir/tmp"
from "src/templates"
}
task processTemplates2(type: ProcessTemplates) {
// ...
sources copyTemplates
}
非常的方便。
如果你不想使用gradle的緩存功能,那么可以使用upToDateWhen()來(lái)手動(dòng)控制:
task alwaysInstrumentClasses(type: Instrument) {
classFiles = layout.files(compileJava)
destinationDir = file("$buildDir/instrumented")
outputs.upToDateWhen { false }
}
上面使用false,表示alwaysInstrumentClasses這個(gè)task將會(huì)一直被執(zhí)行,并不會(huì)使用到緩存。
輸入歸一化
要想比較gradle的輸入是否是一樣的,gradle需要對(duì)input進(jìn)行歸一化處理,然后才進(jìn)行比較。
我們可以自定義gradle的runtime classpath 。
normalization {
runtimeClasspath {
ignore 'build-info.properties'
}
}
上面的例子中,我們忽略了classpath中的一個(gè)文件。
我們還可以忽略META-INF中的manifest文件的屬性:
normalization {
runtimeClasspath {
metaInf {
ignoreAttribute("Implementation-Version")
}
}
}
忽略META-INF/MANIFEST.MF :
normalization {
runtimeClasspath {
metaInf {
ignoreManifest()
}
}
}
忽略META-INF中所有的文件和目錄:
normalization {
runtimeClasspath {
metaInf {
ignoreCompletely()
}
}
}
其他使用技巧
如果你的gradle因?yàn)槟撤N原因暫停了,你可以送 --continuous 或者 -t 參數(shù),來(lái)重用之前的緩存,繼續(xù)構(gòu)建gradle項(xiàng)目。
你還可以使用 --parallel 來(lái)并行執(zhí)行task。
本文已收錄于 http://www.flydean.com/gradle-incremental-build/
最通俗的解讀,最深刻的干貨,最簡(jiǎn)潔的教程,眾多你不知道的小技巧等你來(lái)發(fā)現(xiàn)!
歡迎關(guān)注我的公眾號(hào):「程序那些事」,懂技術(shù),更懂你!