基础知识
Gradle 构建缓存是一种缓存机制,旨在通过重用其他构建产生的输出来节省时间。构建缓存通过存储(本地或远程)构建输出并允许构建在确定输入没有更改时从缓存中获取这些输出来工作,从而避免了重新生成它们的昂贵工作。
使用构建缓存的第一个功能是任务输出缓存。本质上,任务输出缓存利用了与最新检查相同的智能,当先前的本地构建已经产生一组任务输出时,Gradle 使用它来避免工作。但是,任务输出缓存不仅限于同一工作区中的先前构建,而是允许 Gradle 重用本地机器上任何位置的任何早期构建的任务输出。当使用共享构建缓存进行任务输出缓存时,这甚至可以跨开发人员机器和构建代理工作。
除了任务之外,工件转换还可以利用构建缓存并重用其输出,类似于任务输出缓存。
以上内容摘自gradle官方文档,链接如下
我简单的翻译下给各位大佬,在本地存在build cache的情况下,gradle task会基于当前的输入来作为缓存的key值,如果输入内容没有发生变更,则意味着本Task可以被跳过,另外这个不同于增量编译。
又可以偷下官方的图片了。举个栗子,JavaCompiler task
的输入的java文件和上一次编译的一样,则意味着该任务可以使用原来编译输出作为编译产物。
Cacheable tasks
任务类型需要使用 @CacheableTask 注释选择加入任务输出缓存。 请注意,@CacheableTask 不被子类继承。 默认情况下,自定义任务类型不可缓存。
官方有说明什么情况下会使用编译缓存,首先我们的Task
要被定义成@CacheableTask
。
另外对于Task内部的输入和输出也需要打上@TaskInputs
和@TaskOutputs
注解。这样才能保证当前的Task具备了编译缓存的能力。
所以想要写一个能具备缓存能力的Task也是比较复杂的。这也就是为什么Android后面会开始推动Artifacts
的使用了,让开发尽量可以少关心输入输出相关的逻辑。
我们升级适配完AGP 7.0 | 杂谈
那么相对的,没有定义@CacheableTask
的则认为是内有编译缓存的任务。
TaskOutput
在上述这种被跳过的任务哦,一般都会有在Task编译完成之后带上一些特殊的标识符。
-
(no label) or EXECUTED
任务正常执行了。 -
UP-TO-DATE
任务输出没有变更。 - 输入输出均没有发生变更。
- 任务执行了,但是任务告诉gradle输出并未发生变更。
- 任务没有执行和一些依赖项,但所有依赖项都是最新的、已跳过或来自缓存。
- 任务没有执行也没有依赖。
-
FROM-CACHE
任务的输出可以从之前的执行中找到。任务已从构建缓存恢复输出。 -
SKIPPED
该任务没有被执行。任务已明确从命令行中排除。 -
NO-SOURCE
当前无需执行该任务。输入内容并没有源文件,比如.java
简单的来说,除了第一种情况以外,其他的都是任务被跳过。
有趣的编译问题
好了,有了前置的知识储备的情况下,我们就可以展开说一下我们最近碰到的一个奇怪的问题了。
我们有个protobuf
编译的仓库,专门负责将pb文件转化成java或者kotlin。然后会把这些生成的文件移动到另外两个模块进行打包,最后删除生成的所有类文件。然后再去执行javacompiler task。
这个模块出现了一个二次编译的问题。第一次打包protobuf
模块的时候编译是正常的,然后当二次编译该模块的情况下,该模块就会出现类丢失的问题。
问题分析
这个问题分析起来就比较简单。在二次编译的情况下呢,因为输入的内容并没有发生变更,所以触发了Gradle Task
相关的缓存,然后所有的pb文件转化成java kt的过程就被跳过了。但是呢后续的copy task因为本身不具备缓存能力,所以他还是会执行一次cv的任务。但是原来生成的java和kt已经被删除了。这个时候他就会把空的文件夹进行一次覆盖操作。之后就导致了原来的java和kt文件全部丢失的问题。
这就是一个很有趣的build cache
导致的奇形怪状的问题,因为上一个任务具备了编译缓存,之后跳过了编译直接用了原来的output输出。但是呢下一个任务非缓存的,所以必然还是会执行拷贝任务。
至于解决方案我就不写了,感觉大家应该没啥兴趣。