之前介绍了Gradle自定义Task相关的一些内容:Gradle 进阶(一):深入了解 Tasks
其中也提到了,当Task的输入与输出被注解标注并且都没有发生变化时,Task的状态是up-to-date的,此时可以跳过Task的执行
除了以上所说的编译避免的方式,Gradle还提供了其他方案可以优化Task的性能,本文主要包括以下内容
上文提到,当Task的输入与输出被注解标注并且都没有发生变化时,Task的状态是up-to-date的,此时可以跳过Task的执行
但是也存在一种情况,Task的输入只有几个文件发生了更改,而你并不想重新处理其他没有发生更改的文件,这对于那些将输入文件按 1:1 转换为输出文件的Task特别有用,比如我们编译代码的过程,就是将.java文件一对一的编译成.class文件
如果你想要你的Task只重新处理发生了更改的文件,那么可以使用incremental task
对于一个支持增量处理输入的task,必须包含一个处理增量输入的action。我们知道,Task中的Action就是使用@TaskAction注解的方法,与普通的Action不同的是,支持增量的action拥有一个InputChanges的输入。此外,该Task还需要使用@Incremental或@SkipWhenEmpty注解至少一个增量文件输入属性。
增量action可以使用InputChanges.getFileChanges()方法来找出对于指定的输入(类型为RegularFileProperty,DirectoryProperty或ConfigurableFileCollection)中哪些文件已更改。该方法返回一个FileChangesIterable类型的结果,然后可以查询以下内容
下面就是一个支持增量处理的Task示例,它有一个目录文件作为输入,这个task的作用就是将这个目录中的文件都拷贝到另一个目录并且将文件的内容反转。代码如下所示:
abstract class IncrementalReverseTask : DefaultTask() {
@get:Incremental
@get:PathSensitive(PathSensitivity.NAME_ONLY)
@get:InputDirectory
abstract val inputDir: DirectoryProperty
@TaskAction
fun execute(inputChanges: InputChanges) {
println(
if (inputChanges.isIncremental) "Executing incrementally"
else "Executing non-incrementally"
)
inputChanges.getFileChanges(inputDir).forEach { change ->
if (change.fileType == FileType.DIRECTORY) return@forEach
val targetFile = outputDir.file(change.normalizedPath).get().asFile
if (change.changeType == ChangeType.REMOVED) {
targetFile.delete()
} else {
targetFile.writeText(change.file.readText().reversed())
}
}
}
}
可以看出,主要做了以下几点:
总得来说,对于增量编译Task,只需要为任何过时的输入生成输出文件,并为已删除的输入删除输出文件。
如果之前执行过Task,并且自执行以来唯一的更改就是增量输入文件属性,则 Gradle 能够确定需要处理哪些输入文件(即增量执行)。在这种情况下,InputChanges.getFileChanges()方法会返回指定属性的所有已添加、修改或删除的输入文件的详细信息。
但是,在很多情况下,Gradle 无法确定需要处理哪些输入文件(即非增量执行)。比如:
在以上所有这些情况下,Gradle 会将所有的输入文件标记为ADDED状态,并且通过getFileChanges()方法返回
您可以如上面的示例,使用InputChanges.isIncremental()方法检查Task执行是否是增量的。
随着构建的复杂性增加,有时我们很难了解特定值的配置时间和位置。Gradle 提供了几种使用惰性配置来管理这种复杂性的方法。
Gradle 提供了惰性属性,它会延迟属性值的计算,直到实际需要使用该属性的时候,类似于Kotlin的by lazy,惰性属性主要有以下收益
Gradle提供了两个接口表示惰性属性
惰性属性的目的就是在配置阶段传递并且仅在需要时进行查询,而查询通常发生在执行阶段,下面我们来看个例子
abstract class Greeting : DefaultTask() {
// Configurable by the user
@get:Input
abstract val greeting: Property
// Read-only property calculated from the greeting
@Internal
val message: Provider = greeting.map { it + " from Gradle" }
@TaskAction
fun printMessage() {
logger.quiet(message.get())
}
}
tasks.register("greeting") {
// Configure the greeting
greeting.set("Hi")
}
如上所示,该Task有两个属性,一个由用户配置的greeting与一个派生的属性message
上面的示例属性都是常规类型,如果要使用文件类型的惰性属性的话,要怎么处理呢?
Gradle 专门提供了两个Property的子类型来处理文件类型:RegularFileProperty和DirectoryProperty,分别表示惰性的文件和目录
一个DirectoryProperty也可以通过DirectoryProperty.dir(String) 和 DirectoryProperty.file(String)方法创建新的provider,新的provider的路径是相对于创建它的DirectoryProperty 计算的,如下面示例所示
abstract class GenerateSource : DefaultTask() {
@get:InputFile
abstract val configFile: RegularFileProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun compile() {
val inFile = configFile.get().asFile
logger.quiet("configuration file = $inFile")
val dir = outputDir.get().asFile
logger.quiet("output dir = $dir")
}
}
tasks.register("generate") {
// 相对于projectDirectory与buildDirectory配置Task属性
configFile.set(layout.projectDirectory.file("src/config.txt"))
outputDir.set(layout.buildDirectory.dir("generated-source"))
}
// 修改build directory
// 不需要重新配置Task的属性,当build directory变化时,task的outputDir属性会自动变化
layout.buildDirectory.set(layout.projectDirectory.dir("output"))
上面的Task输出如下
> Task :generate
configuration file = /home/user/gradle/samples/kotlin/src/config.txt
output dir = /home/user/gradle/samples/kotlin/output/generated-source
主要需要注意以下两点:
有时我们需要将多个Task连接起来,其中一个Task的输出作为另一个的输入来完成构建。这个时候我们不仅需要指定Task的输入输出,同时需要显式地配置Task之间的依赖关系。如果其中某个属性的依赖关系发生了变化,这可能会很麻烦且脆弱,因为在不使用惰性配置属性的情况下,Task属性需要以正确的顺序配置,并且Task依赖关系也需要手动同步变更
Property API不仅能够像上面示例那样跟踪属性值,并且可以跟踪产生属性的Task,因此我们不必要手动配置Task依赖关系,我们来看下面的例子:
abstract class Producer : DefaultTask() {
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun produce() {
// ...
}
}
abstract class Consumer : DefaultTask() {
@get:InputFile
abstract val inputFile: RegularFileProperty
@TaskAction
fun consume() {
// ...
}
}
val producer = tasks.register("producer")
val consumer = tasks.register("consumer")
consumer {
// 将 producer的输出与consumer的输入建立联系,你不必手动添加Task依赖关系
inputFile.set(producer.flatMap { it.outputFile })
}
producer {
// 更新 producer的值,你不必手动更新 consumer.inputFile,它会随着 producer.outputFile的变化自动变化
outputFile.set(layout.buildDirectory.file("file.txt"))
}
// 修改 build directory,也不必手动更新 producer.outputFile 与 consuer.inputFile,它们会自动更新
layout.buildDirectory.set(layout.projectDirectory.dir("output"))
类似于文件,对于各种集合,Gradle也提供了相应的API供我们使用
这几个接口的使用示例就不在这里缀述了,感兴趣的同学可查看:docs.gradle.org/current/use…
有时我们也可能需要给惰性属性设置一个默认值,在没有为属性配置值时使用
// 创建 property
val property = objects.property(String::class)
// 设置默认值
property.convention("convention 1")
println("value = " + property.get())
// 设置之后也可以重新设置默认值
property.convention("convention 2")
println("value = " + property.get())
// 设置真正的值
property.set("value")
// 一旦设置了真正的值,默认值设置就会被忽略
property.convention("ignored convention")
println("value = " + property.get())
上文我们提到,Provider接口是不可设值的,Property接口是可以设值的。但是,有时候我们希望在Task开始执行之后,禁止对Property的修改(即只在配置阶段修改)
惰性属性提供了finalizeValue()来实现这个需求,它会计算属性的最终值并防止对属性进行进一步更改。当属性的值来自于一个 Provider时,会向提供者查询其当前值,结果将成为该属性的最终值。并且该属性不再跟踪提供者的值。调用此方法还会使属性实例不可修改,并且任何进一步更改属性值的尝试都将失败。
finalizeValueOnRead()方法类似,只是在查询到属性的值之前不计算属性的最终值。换句话说,该方法根据需要延迟计算最终值,而finalizeValue()急切地计算最终值。
本节主要介绍了Provider API的使用,它有着如上文所说的一系列优点,现在官方的Task属性都改成Provider了,我们在开发自定义Task时也应该尽量使用惰性属性
配置避免,简而言之,就是避免在的配置阶段创建和配置Task,因为这些Task可能永远不会被执行。例如,在运行编译Task时,不会执行其他不相关的Task,如代码质量、测试和发布,因此无需花费任何时间创建和配置这些Task。如果在构建过程中不需要配置Task,则配置避免 API 会避免配置Task,这可能会对总配置时间产生重大影响。
配置避免的一个常用手段就是使用register代替create来创建Task,除了这个还有哪些常用手段呢?
避免使用DomainObjectCollection.all(org.gradle.api.Action) 和 DomainObjectCollection.withType(java.lang.Class, org.gradle.api.Action)这样的API,
它们将立即创建和配置任何已注册的Task。要推迟Task配置,您需要迁移到配置避免 API 等效项,即withType(java.lang.Class).configureEach(org.gradle.api.Action)
您可以通过TaskProvider对象来引用已注册的Task,而不是直接引用Task对象。可以通过多种方式获取 TaskProvider,包括调用TaskContainer.register(java.lang.String)或使用TaskCollection.named(java.lang.String)方法。
调用Provider.get()或使用TaskCollection.getByName(java.lang.String)方法将导致立即创建和配置Task。Task.dependsOn (java.lang.Object...)之类的方法的参数可以是TaskProvider,因此您无需取出Task对象
总得来说,即尽量使用TaskProvider而不是Task
依赖关系可以分为软关系与强关系两类
Task.mustRunAfter(…)和Task.shouldRunAfter(…)代表软关系,只能改变现有Task的顺序,不能触发它们的创建。
Task.dependsOn(…)和Task.finalizedBy(…)代表强关系,这将强制执行引用的Task
如果想具体了解我们项目目前Task的配置情况,可以使用gradle --scan命令
可以看到performance的configuration部分,如上图所示:
总得来说,配置避免主要有以下实用手段
在我们自定义Task时,Task的输入有时是个列表,而列表的每一项可以单独处理,这个时候就可以使用Worker API来加速构建
Worker API 提供了将Task操作的执行分解为离散的工作单元然后并发和异步执行该工作的能力。这允许 Gradle 充分利用可用资源并更快地完成构建。
下面我们创建一个自定义Task,该Task为一组可配置的文件生成 MD5 哈希。然后,我们使用Worker API优化该Task
首先我们创建一个Task,该Task为一组可配置的文件生成 MD5 哈希
abstract public class CreateMD5 extends SourceTask {
@OutputDirectory
abstract public DirectoryProperty getDestinationDirectory();
@TaskAction
public void createHashes() {
for (File sourceFile : getSource().getFiles()) {
try {
InputStream stream = new FileInputStream(sourceFile);
System.out.println("Generating MD5 for " + sourceFile.getName() + "...");
// 模拟耗时操作
Thread.sleep(3000);
Provider md5File = getDestinationDirectory().file(sourceFile.getName() + ".md5");
FileUtils.writeStringToFile(md5File.get().getAsFile(), DigestUtils.md5Hex(stream), (String) null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
代码其实很简单,主要是遍历输入文件列表,为每个文件生成md5哈希并输出。如果输入文件数量是3个的话,该Task至少需要 9 秒才能运行,因为它一次对每个文件进行一个哈希处理(每个文件大约 3 秒)。
尽管此Task按顺序处理每个文件,但每个文件的处理独立于任何其他文件。如果这项工作能够并行完成那就太好了。这就是 Worker API 的用武之地
第一步:首先我们需要定义一个接口来表示每个工作单元需要的参数
public interface MD5WorkParameters extends WorkParameters {
RegularFileProperty getSourceFile();
RegularFileProperty getMD5File();
}
第二步:您需要将自定义Task中为每个单独文件执行工作的部分重构为单独的类。这个类是你的“工作单元”实现,它应该是一个继承了WorkAction接口的抽象类
public abstract class GenerateMD5 implements WorkAction {
@Override
public void execute() {
try {
File sourceFile = getParameters().getSourceFile().getAsFile().get();
File md5File = getParameters().getMD5File().getAsFile().get();
InputStream stream = new FileInputStream(sourceFile);
System.out.println("Generating MD5 for " + sourceFile.getName() + "...");
// 模拟耗时操作
Thread.sleep(3000);
FileUtils.writeStringToFile(md5File, DigestUtils.md5Hex(stream), (String) null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
需要注意的是,不要实现getParameters()方法,Gradle 将在运行时注入它。
第三步:您应该重构自定义Task类以将工作提交给 WorkerExecutor,而不是自己完成工作。
abstract public class CreateMD5 extends SourceTask {
@OutputDirectory
abstract public DirectoryProperty getDestinationDirectory();
@Inject
abstract public WorkerExecutor getWorkerExecutor();
@TaskAction
public void createHashes() {
WorkQueue workQueue = getWorkerExecutor().noIsolation();
for (File sourceFile : getSource().getFiles()) {
Provider md5File = getDestinationDirectory().file(sourceFile.getName() + ".md5");
workQueue.submit(GenerateMD5.class, parameters -> {
parameters.getSourceFile().set(sourceFile);
parameters.getMD5File().set(md5File);
});
}
}
}
当我们再次运行这个Task时,可以发现Task运行的速度变快了,这是因为 Worker API 是并行的而不是按顺序对每个文件执行 MD5 计算。
上面的示例中我们使用了noIsolation隔离模式,Gralde提供了三种隔离模式
本文主要介绍了通过支持增量处理,惰性配置,避免不必要的Task配置,以及并行Task等方式来优化自定义Task的性能,希望对你有所帮助~
作者:程序员江同学
链接:https://juejin.cn/post/7140672092550201381
来源:稀土掘金
留言与评论(共有 0 条评论) “” |