1. 程式人生 > >gradle中的增量構建

gradle中的增量構建

[toc] gradle中的增量構建 # 簡介 在我們使用的各種工具中,為了提升工作效率,總會使用到各種各樣的快取技術,比如說docker中的layer就是快取了之前構建的image。在gradle中這種以task組合起來的構建工具也不例外,在gradle中,這種技術叫做增量構建。 # 增量構建 gradle為了提升構建的效率,提出了增量構建的概念,為了實現增量構建,gradle將每一個task都分成了三部分,分別是input輸入,任務本身和output輸出。下圖是一個典型的java編譯的task。 ![](https://img-blog.csdnimg.cn/20201028104251422.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_25,color_8F8F8F,t_70) 以上圖為例,input就是目標jdk的版本,原始碼等,output就是編譯出來的class檔案。 增量構建的原理就是監控input的變化,只有input傳送變化了,才重新執行task任務,否則gradle認為可以重用之前的執行結果。 所以在編寫gradle的task的時候,需要指定task的輸入和輸出。 並且要注意只有會對輸出結果產生變化的才能被稱為輸入,如果你定義了對初始結果完全無關的變數作為輸入,則這些變數的變化會導致gradle重新執行task,導致了不必要的效能的損耗。 還要注意不確定執行結果的任務,比如說同樣的輸入可能會得到不同的輸出結果,那麼這樣的任務將不能夠被配置為增量構建任務。 # 自定義inputs和outputs 既然task中的input和output在增量編譯中這麼重要,本章將會給大家講解一下怎麼才能夠在task中定義input和output。 如果我們自定義一個task型別,那麼滿足下面兩點就可以使用上增量構建了: 第一點,需要為task中的inputs和outputs新增必要的getter方法。 第二點,為getter方法新增對應的註解。 gradle支援三種主要的inputs和outputs型別: 1. 簡單型別:簡單型別就是所有實現了Serializable介面的型別,比如說string和數字。 2. 檔案型別:檔案型別就是 File 或者 FileCollection 的衍生型別,或者其他可以作為引數傳遞給 Project.file(java.lang.Object) 和 Project.files(java.lang.Object...) 的型別。 3. 巢狀型別:有些自定義型別,本身不屬於前面的1,2兩種型別,但是它內部含有巢狀的inputs和outputs屬性,這樣的型別叫做巢狀型別。 接下來,我們來舉個例子,假如我們有一個類似於FreeMarker和Velocity這樣的模板引擎,負責將模板原始檔,要傳遞的資料最後生成對應的填充檔案,我們考慮一下他的輸入和輸出是什麼。 輸入:模板原始檔,模型資料和模板引擎。 輸出:要輸出的檔案。 如果我們要編寫一個適用於模板轉換的task,我們可以這樣寫: ~~~java 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; } // 上面四個屬性的setter方法 @TaskAction public void processTemplates() { // ... } } ~~~ 上面的例子中,我們定義了4個屬性,分別是TemplateEngineType,FileCollection,TemplateData和File。前面三個屬性是輸入,後面一個屬性是輸出。 除了getter和setter方法之外,我們還需要在getter方法中新增相應的註釋: ` @Input , @InputFiles ,@Nested 和 @OutputDirectory `, 除此之外,我們還定義了一個 `@TaskAction` 表示這個task要做的工作。 TemplateEngineType表示的是模板引擎的型別,比如FreeMarker或者Velocity等。我們也可以用String來表示模板引擎的名字。但是為了安全起見,這裡我們自定義了一個列舉型別,在列舉型別內部我們可以安全的定義各種支援的模板引擎型別。 因為enum預設是實現Serializable的,所以這裡可以作為@Input使用。 sourceFiles使用的是FileCollection,表示的是一系列檔案的集合,所以可以使用@InputFiles。 為什麼TemplateData是@Nested型別的呢?TemplateData表示的是我們要填充的資料,我們看下它的實現: ~~~java import java.util.HashMap; import java.util.Map; import org.gradle.api.tasks.Input; public class TemplateData { private String name; private Map variables; public TemplateData(String name, Map variables) { this.name = name; this.variables = new HashMap<>(variables); } @Input public String getName() { return this.name; } @Input public Map getVariables() { return this.variables; } } ~~~ 可以看到,雖然TemplateData本身不是File或者簡單型別,但是它內部的屬性是簡單型別的,所以TemplateData本身可以看做是@Nested的。 outputDir表示的是一個輸出檔案目錄,所以使用的是@OutputDirectory。 使用了這些註解之後,gradle在構建的時候就會檢測和上一次構建相比,這些屬性有沒有傳送變化,如果沒有傳送變化,那麼gradle將會直接使用上一次構建生成的快取。 > 注意,上面的例子中我們使用了FileCollection作為輸入的檔案集合,考慮一種情況,假如只有檔案集合中的某一個檔案傳送變化,那麼gradle是會重新構建所有的檔案,還是隻重構這個被修改的檔案呢? > 留給大家討論 除了上講到的4個註解之外,gradle還提供了其他的幾個有用的註解: * @InputFile: 相當於File,表示單個input檔案。 * @InputDirectory: 相當於File,表示單個input目錄。 * @Classpath: 相當於Iterable,表示的是類路徑上的檔案,對於類路徑上的檔案需要考慮檔案的順序。如果類路徑上的檔案是jar的話,jar中的檔案建立時間戳的修改,並不會影響input。 * @CompileClasspath:相當於Iterable,表示的是類路徑上的java檔案,會忽略類路徑上的非java檔案。 * @OutputFile: 相當於File,表示輸出檔案。 * @OutputFiles: 相當於Map 或者 Iterable,表示輸出檔案。 * @OutputDirectories: 相當於Map 或者 Iterable,表示輸出檔案。 * @Destroys: 相當於File 或者 Iterable,表示這個task將會刪除的檔案。 * @LocalState: 相當於File 或者 Iterable,表示task的本地狀態。 * @Console: 表示屬性不是input也不是output,但是會影響console的輸出。 * @Internal: 內部屬性,不是input也不是output。 * @ReplacedBy: 屬性被其他的屬性替換了,不能算在input和output中。 * @SkipWhenEmpty: 和@InputFiles 跟 @InputDirectory一起使用,如果相應的檔案或者目錄為空的話,將會跳過task的執行。 * @Incremental: 和@InputFiles 跟 @InputDirectory一起使用,用來跟蹤檔案的變化。 * @Optional: 忽略屬性的驗證。 * @PathSensitive: 表示需要考慮paths中的哪一部分作為增量的依據。 # 執行時API 自定義task當然是一個非常好的辦法來使用增量構建。但是自定義task型別需要我們編寫新的class檔案。有沒有什麼辦法可以不用修改task的原始碼,就可以使用增量構建呢? 答案是使用Runtime API。 gradle提供了三個API,用來對input,output和Destroyables進行獲取: * Task.getInputs() of type TaskInputs * Task.getOutputs() of type TaskOutputs * Task.getDestroyables() of type TaskDestroyables 獲取到input和output之後,我們就是可以其進行操作了,我們看下怎麼用runtime API來實現之前的自定義task: ~~~java 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() 相當於 @Input ,而outputs.dir() 相當於@OutputDirectory。 Runtime API還可以和自定義型別一起使用: ~~~java task processTemplatesWithExtraInputs(type: ProcessTemplates) { // ... inputs.file("src/headers/headers.txt") .withPropertyName("headers") .withPathSensitivity(PathSensitivity.NONE) } ~~~ 上面的例子為ProcessTemplates添加了一個input。 # 隱式依賴 除了直接使用dependsOn之外,我們還可以使用隱式依賴: ~~~java task packageFiles(type: Zip) { from processTemplates.outputs } ~~~ 上面的例子中,packageFiles 使用了from,隱式依賴了processTemplates的outputs。 gradle足夠智慧,可以檢測到這種依賴關係。 上面的例子還可以簡寫為: ~~~java task packageFiles2(type: Zip) { from processTemplates } ~~~ 我們看一個錯誤的隱式依賴的例子: ~~~java plugins { id 'java' } task badInstrumentClasses(type: Instrument) { classFiles = fileTree(compileJava.destinationDir) destinationDir = file("$buildDir/instrumented") } ~~~ 這個例子的本意是執行compileJava任務,然後將其輸出的destinationDir作為classFiles的值。 但是因為fileTree本身並不包含依賴關係,所以上面的執行的結果並不會執行compileJava任務。 我們可以這樣改寫: ~~~java task instrumentClasses(type: Instrument) { classFiles = compileJava.outputs.files destinationDir = file("$buildDir/instrumented") } ~~~ 或者使用layout: ~~~java task instrumentClasses2(type: Instrument) { classFiles = layout.files(compileJava) destinationDir = file("$buildDir/instrumented") } ~~~ 或者使用buildBy: ~~~java task instrumentClassesBuiltBy(type: Instrument) { classFiles = fileTree(compileJava.destinationDir) { builtBy compileJava } destinationDir = file("$buildDir/instrumented") } ~~~ # 輸入校驗 gradle會預設對@InputFile ,@InputDirectory 和 @OutputDirectory 進行引數校驗。 如果你覺得這些引數是可選的,那麼可以使用@Optional。 # 自定義快取方法 上面的例子中,我們使用from來進行增量構建,但是from並沒有新增@InputFiles, 那麼它的增量快取是怎麼實現的呢? 我們看一個例子: ~~~java 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,然後又定義了一個sources方法,可以將新的檔案加入到sourceFiles中,從而改變sourceFile input,也就達到了自定義修改input快取的目的。 我們看下怎麼使用: ~~~java task processTemplates(type: ProcessTemplates) { templateEngine = TemplateEngineType.FREEMARKER templateData = new TemplateData("test", [year: 2012]) outputDir = file("$buildDir/genOutput") sources fileTree("src/templates") } ~~~ 我們還可以使用project.layout.files()將一個task的輸出作為輸入,可以這樣做: ~~~java public void sources(Task inputTask) { this.sourceFiles = this.sourceFiles.plus(getProject().getLayout().files(inputTask)); } ~~~ 這個方法傳入一個task,然後使用project.layout.files()將task的輸出作為輸入。 看下怎麼使用: ~~~java task copyTemplates(type: Copy) { into "$buildDir/tmp" from "src/templates" } task processTemplates2(type: ProcessTemplates) { // ... sources copyTemplates } ~~~ 非常的方便。 如果你不想使用gradle的快取功能,那麼可以使用upToDateWhen()來手動控制: ~~~java task alwaysInstrumentClasses(type: Instrument) { classFiles = layout.files(compileJava) destinationDir = file("$buildDir/instrumented") outputs.upToDateWhen { false } } ~~~ 上面使用false,表示alwaysInstrumentClasses這個task將會一直被執行,並不會使用到快取。 # 輸入歸一化 要想比較gradle的輸入是否是一樣的,gradle需要對input進行歸一化處理,然後才進行比較。 我們可以自定義gradle的runtime classpath 。 ~~~java normalization { runtimeClasspath { ignore 'build-info.properties' } } ~~~ 上面的例子中,我們忽略了classpath中的一個檔案。 我們還可以忽略META-INF中的manifest檔案的屬性: ~~~java normalization { runtimeClasspath { metaInf { ignoreAttribute("Implementation-Version") } } } ~~~ 忽略META-INF/MANIFEST.MF : ~~~java normalization { runtimeClasspath { metaInf { ignoreManifest() } } } ~~~ 忽略META-INF中所有的檔案和目錄: ~~~java normalization { runtimeClasspath { metaInf { ignoreCompletely() } } } ~~~ # 其他使用技巧 如果你的gradle因為某種原因暫停了,你可以送 --continuous 或者 -t 引數,來重用之前的快取,繼續構建gradle專案。 你還可以使用 --parallel 來並行執行task。 > 本文已收錄於 [http://www.flydean.com/gradle-incremental-build/](http://www.flydean.com/gradle-incremental-build/) > > 最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現! > > 歡迎關注我的公眾號:「程式那些事」,懂技術,更