Gradle Task UP-TO-DATE
dim.red
环境:Gradle 4.4.1
相关
Task 输入输出注解
@Input,@InputFile,@InputFiles,@InputDirectory, @OutputFile,@OutputFiles,@OutputDirectory,@OutputDirectories,@Destroys,@LocalState,@Nested,@Inject,@OptionValues
@PathSensitive
@Classpath
@CompileClasspath
0x00
Gradle 为了加快构建速度, 加入了快照缓存的概念。
当你的 Task 输出不需要变更。Gradle 会跳转执行过程,同时 Task 在输出打上 UP-TO-DATE 标识。
0x01
怎样的判断一个 Task 输出不需要变更 ?
其中一个条件是比对当前执行状态和上次执行状态的不同。
HistoricalTaskExecution: 表示上次执行状态, 是从快照中反序列化出来的。TaskExecutionSnapshotSerializer.read()
CurrentTaskExecution:表示当前执行状态,是根据当前 Task 的输入输出生成的。 CacheBackedTaskHistoryRepository.createExecution
比对具体逻辑 TaskUpToDateState
1
2
3
4
5
6
7
8
9this.allTaskChanges = new ErrorHandlingTaskStateChanges(task, new SummaryTaskStateChanges(MAX_OUT_OF_DATE_MESSAGES,
previousSuccessState,
noHistoryState,
taskTypeState,
inputPropertiesState,
outputFileChanges,
inputFileChanges,
discoveredInputFilesChanges
));
这里经过 7 个校验,全部验证通过说明这次执行相对上次没有变更,可以直接使用上次执行的输出。
- previousSuccessState:判断之前执行是否成功。
- noHistoryState:判断是否有执行记录。
- taskTypeState:比对 Task 和 Action 的实现。具体是比较 ClassLoader 的 Hash 。
- inputPropertiesState:比对 InputPropert 变更。
- outputFileChanges:比对 OutputFie 变更。
- inputFileChanges:比对 InputFie 变更。
- discoveredInputFilesChanges:比对 Task 中新增的 Input 变更 。
InputPropert
Map\<String, Object> 类型
来自注解 @Input 或API Task.getInput.property
和 Task.getInput.propertys
。
@Input 可以被序列化和反序列化的类型。支持的类型有 基本类型,枚举,Serializable 和 Name 扩展类型。
InputFile
文件类型
来自注解 @InputFile @@InputFiles @InputDirectory 或 APITask.getInput.file
, Task.getInput.files
,Task.getInput.dir
OutputFile
文件类型
来自注解 @OutputDirectory @OutputDirectories @OutputFile @OutputFiles 或 API Task.getInput.dir
,Task.getInput.dirs
,Task.getInput.file
,Task.getInput.files
文件的比较主要分为3种,
- 一般的文件:比较文件的的 Hash,Hash 是由文件 Normalized Name 和 文件的内容计算出来的 MD5。
- Classpath文件: @Classpath 注释。这种类型在计算 jar 的Hash, 会先对 jar 文件里面的 ZipEntry 进行排序再和 Normalized Name 一起算出 MD5. 这样就不会因ZipEntry 排序导致的 MD5 不同。
- CompileClassPath:@CompileClasspath 注释。 在 Classpath 的基础上, 对 Jar 中的 class 进行 ABI 格式化, 即当 jar 提供的接口不变,则 Jar 的 MD5 不变。 这种类型的加入也是使依赖从 compile 升级到 implementation 的关键。
Normalized Name 的策略是注解 @PathSensitive 来确定的。
- ABSOLUTE:文件的绝对位置。
- RELATIVE:文件的相对位置
- NAME_ONLY:文件名
- NONE:忽略
默认值为 ABSOLUTE
具体实现查看代码InputPathNormalizationStrategy
@Nested 是自定义的类型。 是一组或者多组相关输入输出的集合。内部使用上面的注解来定义输入和输出。
注:注解可以声明字段或者 get 方法。但是注解要生效一定要有对应的 get 方法。
0x01
Task 通过注解的方式定义输入和输出。
Gradle 中定义 Task 。
通过接受一个 Class 类型来声明一个 Task 。
Class\
Class\
- ClassGenerator:使用 ASM 对原始的类进行分析,生成 Task 的子类 Task_Decorated,并且实现新的接口,增加新的方法和字段。使之具有扩展的能力。
- TaskFactory:主要设置 Task_Decorated 合适的实例化方法。(为Task 构造方法注入 Service 对象)
- AnnotationProcessingTaskFactory:反射获取 Task 的注解信息,通过一系列的 PropertyAnnotationHandler 处理 Task 类解析出对应 Input 和 Output 。
0x02
Task 的执行由 TaskExecuter 执行的。TaskExecutionServices.createTaskExecuter()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50TaskExecuter createTaskExecuter(TaskArtifactStateRepository repository,
TaskOutputCacheCommandFactory taskOutputCacheCommandFactory,
BuildCacheController buildCacheController,
StartParameter startParameter,
ListenerManager listenerManager,
TaskInputsListener inputsListener,
BuildOperationExecutor buildOperationExecutor,
AsyncWorkTracker asyncWorkTracker,
BuildOutputCleanupRegistry cleanupRegistry,
TaskOutputFilesRepository taskOutputFilesRepository,
BuildScanPluginApplied buildScanPlugin) {
boolean taskOutputCacheEnabled = startParameter.isBuildCacheEnabled();
boolean scanPluginApplied = buildScanPlugin.isBuildScanPluginApplied();
TaskOutputsGenerationListener taskOutputsGenerationListener = listenerManager.getBroadcaster(TaskOutputsGenerationListener.class);
TaskExecuter executer = new ExecuteActionsTaskExecuter(
taskOutputsGenerationListener,
listenerManager.getBroadcaster(TaskActionListener.class),
buildOperationExecutor,
asyncWorkTracker
);
boolean verifyInputsEnabled = Boolean.getBoolean("org.gradle.tasks.verifyinputs");
if (verifyInputsEnabled) {
executer = new VerifyNoInputChangesTaskExecuter(repository, executer);
}
executer = new OutputDirectoryCreatingTaskExecuter(executer);
if (taskOutputCacheEnabled) {
executer = new SkipCachedTaskExecuter(
buildCacheController,
taskOutputsGenerationListener,
taskOutputCacheCommandFactory,
executer
);
}
executer = new SkipUpToDateTaskExecuter(executer);
executer = new ResolveTaskOutputCachingStateExecuter(taskOutputCacheEnabled, executer);
if (verifyInputsEnabled || taskOutputCacheEnabled || scanPluginApplied) {
executer = new ResolveBuildCacheKeyExecuter(executer, buildOperationExecutor);
}
executer = new ValidatingTaskExecuter(executer);
executer = new SkipEmptySourceFilesTaskExecuter(inputsListener, cleanupRegistry, taskOutputsGenerationListener, executer);
executer = new CleanupStaleOutputsExecuter(cleanupRegistry, taskOutputFilesRepository, buildOperationExecutor, executer);
executer = new ResolveTaskArtifactStateTaskExecuter(repository, executer);
executer = new SkipTaskWithNoActionsExecuter(executer);
executer = new SkipOnlyIfTaskExecuter(executer);
executer = new ExecuteAtMostOnceTaskExecuter(executer);
executer = new CatchExceptionTaskExecuter(executer);
return executer;
}
这是一个装饰者模式。
- CatchExceptionTaskExecuter:拦截执行中出现的异常。
- ExecuteAtMostOnceTaskExecuter:确保 Task 只执行一次。
- SkipOnlyIfTaskExecuter:支持 Task.OnlyIf . Task.onlyIf 为 false 将跳过该任务的执行。
- SkipTaskWithNoActionsExecuter:过滤没有 Action 的 Task。(默认第一个 Action 是 Task 中的被 @TaskAction 的方法)
- ResolveTaskArtifactStateTaskExecuter:从快照中反序列化出上次执行的状态(HistoricalTaskExecution)。
- CleanupStaleOutputsExecuter:负责清除非 Task 执行中生成的文件
- SkipEmptySourceFilesTaskExecuter:判断存在 Output 存在时 Source 文件不为空。Source 文件是输入文件中被 @SkipWhenEmpty 注释的属性。
- ValidatingTaskExecuter:验证 input 和 output 。比如 input 的文件要存在等等。
- ResolveBuildCacheKeyExecuter:计算当前执行 Task 的 CacheKey,基于 TaskPath,input , output , Action 等信息, 作为后面从缓存中获取数据的 key。
- ResolveTaskOutputCachingStateExecuter: 设置 Task Output 的缓存状态。
- SkipUpToDateTaskExecuter:是否能直接跳过执行过程,逻辑主要是有几个, 一,task 的输入输出没有变更, 二 Task 输出 upToDate 为true,三,Gradle 执行命令没有使用 rerun-tasks 参数, 同时执行成功会保存当前 Task 状态(CurrentTaskExecution)的快照, 其中包括 InputPropert ,InputFile,OutputFile。 InputPropert 是将它序列化,而 InputFile,OutputFile 是保存的的文件的 Normalized Name 和 Hash,并不保存文件本身。默认保存在项目中(.gradle/4.4.1/taskHistory/taskHistory.bin)文件下。而这些将成为下一个执行时候从
ResolveTaskArtifactStateTaskExecuter
反序列化出来。 - SkipCachedTaskExecuter:缓存有效的时候。尝试从根据 CacheKey 把 OutputFile 文件加载进来, 这里的缓存的来源可以是本地的文件也可是是远程的 Http 服务。同时在 Task 执行完成以后,将Task 输出缓存起来,同样可是缓存在本地或者远程。
默认情况本地存储是开启的,位置在全局的 .gradle/caches/build-cache-1/,
远程 Http 服务是关闭的。 - OutputDirectoryCreatingTaskExecuter:Output 文件不存在自动创建
- VerifyNoInputChangesTaskExecuter:验证输入在执行过程中是否有变更。
- ExecuteActionsTaskExecuter: 执行被注解 @TaskAction 的方法和添加进来的 Action 。
需要注意的是这里有两种东西,一种是快照由 SkipUpToDateTaskExecuter 存储的是执行的状态,不包括 Output 的实体。另一种缓存是由 SkipCachedTaskExecuter 存储,是 Output 的实体而不是状态。
0x03
当 Task 满足以下 4 个条件其中一个,则 Gradle 跳转执行过程。
- 当一个 Task 定义了输出,Task 的 Output.upToDate 为 true,Task Source 为空。Gradle 将跳过该任务的执行。 Output 被标识 NO-SOURCE, Output 为空。
- 当一个 Task 定义了输出,Task 的 Output.upToDate 为 true,Task Source 不为空, Task 的输入和输出没有变更。Output 被标识 UP-TO-DATE,Gradle 将跳过该任务的执行。 使用上次的 Output 。
- Task 的 OnlyIf 为 false, Gradle 将跳过该任务的执行。Output 被标识 SKIPPED Output 为空。
- 支持缓存。缓存存在且有效,Gradle 将跳过该任务的执行。Output 被标识 FROM-CACHE,使用从缓存解压的 Output 。
0x04 尾巴
Gradle 的代码相对比较松散, 而 Task 这块的代码相对比较集中。通过本章当中的一些关键节点可以很方便的进行学习和深入了解。