Gradle Task UP-TO-DATE

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 这块的代码相对比较集中。通过本章当中的一些关键节点可以很方便的进行学习和深入了解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容