Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >gradle中的增量构建

gradle中的增量构建

作者头像
子润先生
修改于 2021-06-21 03:23:05
修改于 2021-06-21 03:23:05
82300
代码可运行
举报
运行总次数:0
代码可运行

gradle中的增量构建

简介

在我们使用的各种工具中,为了提升工作效率,总会使用到各种各样的缓存技术,比如说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类型:

  1. 简单类型:简单类型就是所有实现了Serializable接口的类型,比如说string和数字。
  2. 文件类型:文件类型就是 File 或者 FileCollection 的衍生类型,或者其他可以作为参数传递给 Project.file(java.lang.Object) 和 Project.files(java.lang.Object…) 的类型。
  3. 嵌套类型:有些自定义类型,本身不属于前面的1,2两种类型,但是它内部含有嵌套的inputs和outputs属性,这样的类型叫做嵌套类型。

接下来,我们来举个例子,假如我们有一个类似于FreeMarker和Velocity这样的模板引擎,负责将模板源文件,要传递的数据最后生成对应的填充文件,我们考虑一下他的输入和输出是什么。

输入:模板源文件,模型数据和模板引擎。

输出:要输出的文件。

如果我们要编写一个适用于模板转换的task,我们可以这样写:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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表示的是我们要填充的数据,我们看下它的实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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或者简单类型,但是它内部的属性是简单类型的,所以TemplateData本身可以看做是@Nested的。

outputDir表示的是一个输出文件目录,所以使用的是@OutputDirectory。

使用了这些注解之后,gradle在构建的时候就会检测和上一次构建相比,这些属性有没有发送变化,如果没有发送变化,那么gradle将会直接使用上一次构建生成的缓存。

注意,上面的例子中我们使用了FileCollection作为输入的文件集合,考虑一种情况,假如只有文件集合中的某一个文件发送变化,那么gradle是会重新构建所有的文件,还是只重构这个被修改的文件呢? 留给大家讨论

除了上讲到的4个注解之外,gradle还提供了其他的几个有用的注解:

  • @InputFile: 相当于File,表示单个input文件。
  • @InputDirectory: 相当于File,表示单个input目录。
  • @Classpath: 相当于Iterable<File>,表示的是类路径上的文件,对于类路径上的文件需要考虑文件的顺序。如果类路径上的文件是jar的话,jar中的文件创建时间戳的修改,并不会影响input。
  • @CompileClasspath:相当于Iterable<File>,表示的是类路径上的java文件,会忽略类路径上的非java文件。
  • @OutputFile: 相当于File,表示输出文件。
  • @OutputFiles: 相当于Map<String, File> 或者 Iterable<File>,表示输出文件。
  • @OutputDirectories: 相当于Map<String, File> 或者 Iterable<File>,表示输出文件。
  • @Destroys: 相当于File 或者 Iterable<File>,表示这个task将会删除的文件。
  • @LocalState: 相当于File 或者 Iterable<File>,表示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:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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还可以和自定义类型一起使用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
task processTemplatesWithExtraInputs(type: ProcessTemplates) {
    // ...

    inputs.file("src/headers/headers.txt")
        .withPropertyName("headers")
        .withPathSensitivity(PathSensitivity.NONE)
}

上面的例子为ProcessTemplates添加了一个input。

隐式依赖

除了直接使用dependsOn之外,我们还可以使用隐式依赖:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
task packageFiles(type: Zip) {
    from processTemplates.outputs
}

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

gradle足够智能,可以检测到这种依赖关系。

上面的例子还可以简写为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
task packageFiles2(type: Zip) {
    from processTemplates
}

我们看一个错误的隐式依赖的例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
plugins {
    id 'java'
}

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

这个例子的本意是执行compileJava任务,然后将其输出的destinationDir作为classFiles的值。

但是因为fileTree本身并不包含依赖关系,所以上面的执行的结果并不会执行compileJava任务。

我们可以这样改写:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
task instrumentClasses(type: Instrument) {
    classFiles = compileJava.outputs.files
    destinationDir = file("$buildDir/instrumented")
}

或者使用layout:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
task instrumentClasses2(type: Instrument) {
    classFiles = layout.files(compileJava)
    destinationDir = file("$buildDir/instrumented")
}

或者使用buildBy:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
task instrumentClassesBuiltBy(type: Instrument) {
    classFiles = fileTree(compileJava.destinationDir) {
        builtBy compileJava
    }
    destinationDir = file("$buildDir/instrumented")
}

输入校验

gradle会默认对@InputFile ,@InputDirectory 和 @OutputDirectory 进行参数校验。

如果你觉得这些参数是可选的,那么可以使用@Optional。

自定义缓存方法

上面的例子中,我们使用from来进行增量构建,但是from并没有添加@InputFiles, 那么它的增量缓存是怎么实现的呢?

我们看一个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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缓存的目的。

我们看下怎么使用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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的输出作为输入,可以这样做:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public void sources(Task inputTask) {
        this.sourceFiles = this.sourceFiles.plus(getProject().getLayout().files(inputTask));
    }

这个方法传入一个task,然后使用project.layout.files()将task的输出作为输入。

看下怎么使用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
task copyTemplates(type: Copy) {
    into "$buildDir/tmp"
    from "src/templates"
}

task processTemplates2(type: ProcessTemplates) {
    // ...
    sources copyTemplates
}

非常的方便。

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
task alwaysInstrumentClasses(type: Instrument) {
    classFiles = layout.files(compileJava)
    destinationDir = file("$buildDir/instrumented")
    outputs.upToDateWhen { false }
}

上面使用false,表示alwaysInstrumentClasses这个task将会一直被执行,并不会使用到缓存。

输入归一化

要想比较gradle的输入是否是一样的,gradle需要对input进行归一化处理,然后才进行比较。

我们可以自定义gradle的runtime classpath 。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
normalization {
    runtimeClasspath {
        ignore 'build-info.properties'
    }
}

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

我们还可以忽略META-INF中的manifest文件的属性:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
normalization {
    runtimeClasspath {
        metaInf {
            ignoreAttribute("Implementation-Version")
        }
    }
}

忽略META-INF/MANIFEST.MF :

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
normalization {
    runtimeClasspath {
        metaInf {
            ignoreManifest()
        }
    }
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
normalization {
    runtimeClasspath {
        metaInf {
            ignoreCompletely()
        }
    }
}

其他使用技巧

如果你的gradle因为某种原因暂停了,你可以送 –continuous 或者 -t 参数,来重用之前的缓存,继续构建gradle项目。

你还可以使用 –parallel 来并行执行task。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
其实 Gradle Transform 就是个纸老虎
目前,使用 AGP Transform API 进行字节码插桩已经非常普遍了,例如 Booster、神策等框架中都有 Transform 的影子。Transform 听起来很高大上,其本质就是一个 Gradle Task。在这篇文章里,我将带你理解 Transform 的工作机制、使用方法和核心源码解析,并通过一个 Demo 帮助你融会贯通。
用户9995743
2022/09/26
1.1K0
其实 Gradle Transform 就是个纸老虎
深入探索 Android Gradle 插件的缓存配置
配置缓存是一个提升 IDE 和命令行构建速度的基础构建块。这是 Gradle 6.6 版本提供的一个高度实验性功能,它可以使构建系统记录一次任务的图谱信息,并在接下来的构建中进行复用,从而避免再一次配置整个工程。这一功能也是配置阶段改进的延续,这些改进中引入了 惰性配置 (lazy configuration),以避免在构建的配置阶段进行不必要的工作。这些改进对于快速迭代开发的重要性不言自明,而后者也是 Android Studio 团队所持续关注的一个用例。
Android 开发者
2020/11/16
2.4K0
深入探索 Android Gradle 插件的缓存配置
Gradle 进阶学习之 文件操作
在 Gradle 中,Project.file(java.lang.Object) 方法是一个非常有用的工具,它允许你以一种类型安全的方式引用文件。这个方法可以接收一个字符串路径,返回一个 File 对象,这个对象代表的是一个相对于当前项目目录(或者子项目目录)的文件或目录,或者是指定的绝对路径。
叫我阿杰好了
2024/04/25
1560
Gradle 进阶学习之 文件操作
Gradle Authoring Tasks
在入门教程中,您学习了如何创建简单的任务。 稍后您还学习了如何向这些任务添加额外的行为,并学习了如何在任务之间创建依赖关系。 这一切都是关于简单的任务,但 Gradle 把任务的概念更进一步。 Gradle 支持增强型任务,这些任务具有自己的属性和方法。 这与您习惯使用 Ant 目标的情况大不相同。 这些强化的任务要么是你提供的,要么是内置在 Gradle 的。
acc8226
2022/05/17
8340
为什么说 Gradle 是 Android 进阶绕不去的坎
Gradle 作为官方主推的构建系统,目前已经深度应用于 Android 的多个技术体系中,例如组件化开发、产物构建、单元测试等。可见,要成为 Android 高级工程师 Gradle 是必须掌握的知识点。在这篇文章里,我将带你由浅入深建立 Gradle 的基本概念,涉及 Gradle 生命周期、Project、Task 等知识点,这些内容也是 Gradle 在面试八股文中容易遇见的问题。
用户9995743
2022/09/26
2.6K0
为什么说 Gradle 是 Android 进阶绕不去的坎
Gradle Builds Everything —— Task 实例
为了方便,我们的语境分不开 Gradle和 AndroidGradlePlugin,因此此处不脱离 Android环境来介绍 Gradle。我们在讲述任务依赖的时候,提到一个 Manager的东西,在这里,我们说到的是 AndroidGradlePlugin提供的 BuildableArtifactsHolder这个类。
程序亦非猿
2019/11/23
7630
构建的抽象
不同编程语言编写的应用,在它运行的状态下,会有不同的运行机制,有的是以二进制的方式运行的,有运行在编程语言的虚拟机之上。而构建所做的事情呢,就是将那些我们写给人类看的代码,转换为机器/程序能看懂的代码。所以,构建的本质就是翻译(~~复读机~~)。
Phodal
2020/09/10
9760
Android静态代码扫描效率优化与实践
小伙伴们,美美又来推荐干货文章啦~本文主要介绍Android静态扫描工具Lint、CheckStyle、FindBugs在扫描效率优化上的一些探索和实践,希望大家喜欢鸭。
美团技术团队
2019/11/10
1.7K0
Android静态代码扫描效率优化与实践
Gradle 中的文件操作
使用 Project.file(java.lang.Object)方法,通过指定 文件的相对路径或绝对路径 来对文件的操作,其中相对路径为相对当前 project[根 project 或者子 project]的目录。其实使用 Project.file(java.lang.Object)方法创建的 File 对象就是 Java 中的 File 对象,我们可以使用它就像在 Java 中使用一样。示例代码如下:
鱼找水需要时间
2023/02/16
7540
工具篇 | Gradle入门与使用指南 - 附Github仓库地址
Gradle是一个开源构建自动化工具,专为大型项目设计。它基于DSL(领域特定语言)编写,该语言是用Groovy编写的,使得构建脚本更加简洁和强大。Gradle不仅可以构建Java应用程序,还支持多种语言和技术,例如C++、Python、Android等。
kfaino
2023/09/26
3.5K0
工具篇 | Gradle入门与使用指南 - 附Github仓库地址
【Android Gradle 插件】自定义 Gradle 任务 ⑮ ( Gradle 自带 Zip 任务使用 | Zip 任务简介 | 代码示例 )
org.gradle.api.tasks.bundling.Zip 自带任务 ( 任务类型 ) 文档 :https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Zip.html
韩曙亮
2023/03/30
7340
【Android Gradle 插件】自定义 Gradle 任务 ⑮ ( Gradle 自带 Zip 任务使用 | Zip 任务简介 | 代码示例 )
再写个Gradle脚本干活去,解放双手前言Gradle 脚本
前言 上一篇写个批处理来帮忙干活---遍历&字符串处理中,我们已经学习如何写批处理脚本来帮我们做一些简单的重复性工作,本篇继续来学习如何用 Gradle 写脚本,让它也来帮我们干活 Gradle 脚本 需求场景跟上一篇一样,只是需要脚本能够帮我们遍历某个目录下的文件,然后分别针对每个文件执行 java 命令,再输出新的命名格式的文件即可,因此脚本涉及的方面仍然是:文件夹的遍历操作、字符串处理、执行 java 命令。下面开始学习吧: 1. 遍历指定文件夹下的文件 1.1 files() 命令: files(f
请叫我大苏
2018/06/19
2.5K0
Gradle Spring Intellij Idea下热部署实现“敏捷”开发 | TW洞见
今日洞见 文章作者来自ThoughtWorks:朱本威。 本文所有内容,包括文字、图片和音视频资料,版权均属ThoughtWorks公司所有,任何媒体、网站或个人未经本网协议授权不得转载、链接、转贴或以其他方式复制发布/发表。已经本网协议授权的媒体、网站,在使用时必须注明"内容来源:ThoughtWorks洞见",并指定原文链接,违者本网将依法追究责任。 #百万奖金有奖问答#程序员的什么最值钱? 是他/她们的聪明才智,简洁代码,惊艳的颜值,还是无与伦比的手速,都不是,是宝贵的时间。 如果,你有机会尝试纯前端
ThoughtWorks
2018/04/20
1.8K0
使用Gradle自定义配置构建Java程序
某些情况下默认的源代码路径等可能不符合我们项目的结构,这时除了修改项目结构外,我们还可以自定义源代码路径等配置。
三产
2021/01/12
9150
【Android Gradle 插件】自定义 Gradle 任务 ⑬ ( DefaultTask 中的任务输入和输出属性 | TaskInputs 任务输入接口 | FileCollection )
DefaultTask 又继承了 AbstractTask 类 , 在 AbstractTask 类中 , 有 taskInputs 和 taskOutputs 两个成员变量 , 分别代表任务的 输入 和 输出 ;
韩曙亮
2023/03/30
1.3K0
【Android Gradle 插件】自定义 Gradle 任务 ⑬ ( DefaultTask 中的任务输入和输出属性 | TaskInputs 任务输入接口 | FileCollection )
Gradle教程和指南 – 创建Gradle构建
遵循本指南,你将创建一个Gradle项目,调用一些基本的Gradle命令,并了解Gradle如何管理项目。
全栈程序员站长
2022/09/14
2K0
Gradle教程和指南 – 创建Gradle构建
Gradle Java 插件
Java 插件是构建 JVM 项目的基础,它为项目增加了很多能力,例如编译,测试,打包,发布等等。 很多插件都是基于 Java 插件实现的,例如 Android 插件。
佛系编码
2019/12/11
1.4K0
Gradle Java 插件
Gradle 之 Task 使用
在根工程下自定义config.gradle可以直接在根project引用apply from:'config.gradle' 如果需要在app project中引用,需要加rootProject,表明当前gradle路径在根工程下,apply from: this.rootProject.file('releaseinfo.gradle')
Yif
2019/12/26
9190
Android Gradle配置分析
Android 开发目前大家使用的IDE是Android Studio,所以和Gradle打交道就是必不可少的了。 大部分时间可能我们关注的都是业务代码的开发,然而了解gradle可以帮助我们更好的构建我们的项目
艳龙
2021/12/16
9960
Android Gradle配置分析
使用新 Android Gradle 插件加速您的应用构建
自 2020 年底,Android Gradle 插件 (AGP) 已经开始使用新的版本号规则,其版本号将与 Gradle 主要版本号保持一致,因此 AGP 4.2 之后的版本为 7.0 (目前最新的版本为 7.2)。在更新 Android Studio 时,您可能会收到一并将 Gradle 更新为最新可用版本的提示。为了获得最佳性能,建议您使用 Gradle 和 Android Gradle 插件这两者的最新版本。Android Gradle 插件的 7.0 版本更新带来了许多实用的特性,本文将着重为您介绍其中的 Gradle 性能改进、配置缓存和插件扩展等方面的内容。
Android 开发者
2022/04/01
2.7K0
使用新 Android Gradle 插件加速您的应用构建
推荐阅读
相关推荐
其实 Gradle Transform 就是个纸老虎
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验