美文网首页gradle实战
Gradle Task UP-TO-DATE

Gradle Task UP-TO-DATE

作者: 轻微 | 来源:发表于2018-08-24 13:38 被阅读449次

    dim.red
    环境:Gradle 4.4.1

    相关

    Task 输入输出注解
    @Input,@InputFile,@InputDirectory,@InputFiles, @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

    this.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.propertyTask.getInput.propertys

    @Input 可以被序列化和反序列化的类型。支持的类型有 基本类型,枚举,Serializable 和 Name 扩展类型。


    image.png

    InputFile

    文件类型
    来自注解 @InputDirectory @InputFile 或 API
    Task.getInput.fileTask.getInput.filesTask.getInput.dir

    OutputFile

    文件类型
    来自注解 @OutputDirectory @OutputDirectories @OutputFile @OutputFiles 或 API Task.getInput.dirTask.getInput.dirsTask.getInput.fileTask.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 方法,而不是字段上面。

    0x01

    Task 通过注解的方式定义输入和输出。
    Gradle 中定义 Task 。


    image.png

    通过接受一个 Class 类型来声明一个 Task 。
    Class<Task> -> Class<Task_Decorated> -> Task_Decorated

    image.png

    Class<Task> 会经过 ClassGenerator , TaskFactory,AnnotationProcessingTaskFactory 生成 Task_Decorated 对象。Task_Decorated 是对 Task 的扩展。

    • ClassGenerator:使用 ASM 对原始的类进行分析,生成 Task 的子类 Task_Decorated,并且实现新的接口,增加新的方法和字段。使之具有扩展的能力。
    • TaskFactory:主要设置 Task_Decorated 合适的实例化方法。(为Task 构造方法注入 Service 对象)
    • AnnotationProcessingTaskFactory:反射获取 Task 的注解信息,通过一系列的 PropertyAnnotationHandler 处理 Task 类解析出对应 Input 和 Output 。

    0x02

    Task 的执行由 TaskExecuter 执行的。
    TaskExecutionServices.createTaskExecuter()

    TaskExecuter 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 跳转执行过程。

    1. 当一个 Task 定义了输出,Task 的 Output.upToDate 为 true,Task Source 为空。Gradle 将跳过该任务的执行。 Output 被标识 NO-SOURCE, Output 为空。
    2. 当一个 Task 定义了输出,Task 的 Output.upToDate 为 true,Task Source 不为空, Task 的输入和输出没有变更。Output 被标识 UP-TO-DATE,Gradle 将跳过该任务的执行。 使用上次的 Output 。
    3. Task 的 OnlyIf 为 false, Gradle 将跳过该任务的执行。Output 被标识 SKIPPED Output 为空。
    4. 支持缓存。缓存存在且有效,Gradle 将跳过该任务的执行。Output 被标识 FROM-CACHE,使用从缓存解压的 Output 。

    0x04 尾巴

    Gradle 的代码相对比较松散, 而 Task 这块的代码相对比较集中。通过本章当中的一些关键节点可以很方便的进行学习和深入了解。

    相关文章

      网友评论

        本文标题:Gradle Task UP-TO-DATE

        本文链接:https://www.haomeiwen.com/subject/kywsiftx.html