日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

gradle中的增量构建

發布時間:2024/2/28 编程问答 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 gradle中的增量构建 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

  • 簡介
  • 增量構建
  • 自定義inputs和outputs
  • 運行時API
  • 隱式依賴
  • 輸入校驗
  • 自定義緩存方法
  • 輸入歸一化
  • 其他使用技巧

簡介

在我們使用的各種工具中,為了提升工作效率,總會使用到各種各樣的緩存技術,比如說docker中的layer就是緩存了之前構建的image。在gradle中這種以task組合起來的構建工具也不例外,在gradle中,這種技術叫做增量構建。

增量構建

gradle為了提升構建的效率,提出了增量構建的概念,為了實現增量構建,gradle將每一個task都分成了三部分,分別是input輸入,任務本身和output輸出。下圖是一個典型的java編譯的task。

以上圖為例,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類型:

  • 簡單類型:簡單類型就是所有實現了Serializable接口的類型,比如說string和數字。

  • 文件類型:文件類型就是 File 或者 FileCollection 的衍生類型,或者其他可以作為參數傳遞給 Project.file(java.lang.Object) 和 Project.files(java.lang.Object…) 的類型。

  • 嵌套類型:有些自定義類型,本身不屬于前面的1,2兩種類型,但是它內部含有嵌套的inputs和outputs屬性,這樣的類型叫做嵌套類型。

  • 接下來,我們來舉個例子,假如我們有一個類似于FreeMarker和Velocity這樣的模板引擎,負責將模板源文件,要傳遞的數據最后生成對應的填充文件,我們考慮一下他的輸入和輸出是什么。

    輸入:模板源文件,模型數據和模板引擎。

    輸出:要輸出的文件。

    如果我們要編寫一個適用于模板轉換的task,我們可以這樣寫:

    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;@Inputpublic TemplateEngineType getTemplateEngine() {return this.templateEngine;}@InputFilespublic FileCollection getSourceFiles() {return this.sourceFiles;}@Nestedpublic TemplateData getTemplateData() {return this.templateData;}@OutputDirectorypublic File getOutputDir() { return this.outputDir; }// 上面四個屬性的setter方法@TaskActionpublic 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表示的是我們要填充的數據,我們看下它的實現:

    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);}@Inputpublic String getName() { return this.name; }@Inputpublic Map<String, String> 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<String, File> 或者 Iterable,表示輸出文件。

    • @OutputDirectories: 相當于Map<String, File> 或者 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:

    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還可以和自定義類型一起使用:

    task processTemplatesWithExtraInputs(type: ProcessTemplates) {// ...inputs.file("src/headers/headers.txt").withPropertyName("headers").withPathSensitivity(PathSensitivity.NONE) }

    上面的例子為ProcessTemplates添加了一個input。

    隱式依賴

    除了直接使用dependsOn之外,我們還可以使用隱式依賴:

    task packageFiles(type: Zip) {from processTemplates.outputs }

    上面的例子中,packageFiles 使用了from,隱式依賴了processTemplates的outputs。

    gradle足夠智能,可以檢測到這種依賴關系。

    上面的例子還可以簡寫為:

    task packageFiles2(type: Zip) {from processTemplates }

    我們看一個錯誤的隱式依賴的例子:

    plugins {id 'java' }task badInstrumentClasses(type: Instrument) {classFiles = fileTree(compileJava.destinationDir)destinationDir = file("$buildDir/instrumented") }

    這個例子的本意是執行compileJava任務,然后將其輸出的destinationDir作為classFiles的值。

    但是因為fileTree本身并不包含依賴關系,所以上面的執行的結果并不會執行compileJava任務。

    我們可以這樣改寫:

    task instrumentClasses(type: Instrument) {classFiles = compileJava.outputs.filesdestinationDir = 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") }

    輸入校驗

    gradle會默認對@InputFile ,@InputDirectory 和 @OutputDirectory 進行參數校驗。

    如果你覺得這些參數是可選的,那么可以使用@Optional。

    自定義緩存方法

    上面的例子中,我們使用from來進行增量構建,但是from并沒有添加@InputFiles, 那么它的增量緩存是怎么實現的呢?

    我們看一個例子:

    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緩存的目的。

    我們看下怎么使用:

    task processTemplates(type: ProcessTemplates) {templateEngine = TemplateEngineType.FREEMARKERtemplateData = new TemplateData("test", [year: 2012])outputDir = file("$buildDir/genOutput")sources fileTree("src/templates") }

    我們還可以使用project.layout.files()將一個task的輸出作為輸入,可以這樣做:

    public void sources(Task inputTask) {this.sourceFiles = this.sourceFiles.plus(getProject().getLayout().files(inputTask));}

    這個方法傳入一個task,然后使用project.layout.files()將task的輸出作為輸入。

    看下怎么使用:

    task copyTemplates(type: Copy) {into "$buildDir/tmp"from "src/templates" }task processTemplates2(type: ProcessTemplates) {// ...sources copyTemplates }

    非常的方便。

    如果你不想使用gradle的緩存功能,那么可以使用upToDateWhen()來手動控制:

    task alwaysInstrumentClasses(type: Instrument) {classFiles = layout.files(compileJava)destinationDir = file("$buildDir/instrumented")outputs.upToDateWhen { false } }

    上面使用false,表示alwaysInstrumentClasses這個task將會一直被執行,并不會使用到緩存。

    輸入歸一化

    要想比較gradle的輸入是否是一樣的,gradle需要對input進行歸一化處理,然后才進行比較。

    我們可以自定義gradle的runtime classpath 。

    normalization {runtimeClasspath {ignore 'build-info.properties'} }

    上面的例子中,我們忽略了classpath中的一個文件。

    我們還可以忽略META-INF中的manifest文件的屬性:

    normalization {runtimeClasspath {metaInf {ignoreAttribute("Implementation-Version")}} }

    忽略META-INF/MANIFEST.MF :

    normalization {runtimeClasspath {metaInf {ignoreManifest()}} }

    忽略META-INF中所有的文件和目錄:

    normalization {runtimeClasspath {metaInf {ignoreCompletely()}} }

    其他使用技巧

    如果你的gradle因為某種原因暫停了,你可以送 --continuous 或者 -t 參數,來重用之前的緩存,繼續構建gradle項目。

    你還可以使用 --parallel 來并行執行task。

    本文已收錄于 http://www.flydean.com/gradle-incremental-build/

    最通俗的解讀,最深刻的干貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

    歡迎關注我的公眾號:「程序那些事」,懂技術,更懂你!

    總結

    以上是生活随笔為你收集整理的gradle中的增量构建的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。