前言:增量编译本质上需要解决两个问题,第一个是增删修改文件的检测,第二个是文件的依赖关系检索,其中增删修改文件的检测又是整个增量编译的基础,是最核心最关键的环节,本文将会给大家介绍下怎么通过Gradle去检测增删修改文件
目前Gradle最新版本是5.4,在5.4版本以前Gradle通过IncrementalTaskInputs这个API来检测增删修改文件的,但是由于这个API存在着一些弊端5.4之后已经被标记为deprecated了,后面可能还会删除掉此接口,所以这里我们只介绍新的API方法。5.4之后Gradle提供了 InputChanges @Incremental @SkipWhenEmpty 这几个API来实现增删修改文件的检测能力,下面介绍下如何去使用这些API。
首先写一个自定义Task,Task的Action Method必须有一个类型为InputChanges的参数,Gradle会通过这个参数来告诉用户文件是增加了还是被删除了还是被修改了,除此以外Task必须通过@Incremental来定义至少一个输入参数,通常是个目录名称等可以通过RegularFileProperty, DirectoryProperty 又或者 ConfigurableFileCollection来指定
。当Task Action被执行的时候,我们就可以通过InputChanges提供的getFileChanges() 方法来获取到哪些文件是被修改过了,说了这么多了,是不是有种一脸懵逼的感觉了,下面我们还是来写一些简单的例子来演示下Incremental Task的使用吧。
abstract class IncrementalTask extends DefaultTask {
@Incremental
@PathSensitive(PathSensitivity.NAME_ONLY)
@InputDirectory
abstract DirectoryProperty getInputDir()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@TaskAction
void taskAction(InputChanges inputChanges) {
println(inputChanges.incremental
? 'Executing incrementally'
: 'Executing non-incrementally'
)
inputChanges.getFileChanges(inputDir).each { change ->
//跳过目录
if (change.fileType == FileType.DIRECTORY) return
println "${change.changeType}: ${change.normalizedPath}"
//outDir查找这个文件.
def targetFile = outputDir.file(change.normalizedPath).get().asFile
//如果文件是被移出了outDir也对应的移出下.
if (change.changeType == ChangeType.REMOVED) {
targetFile.delete()
} else {
//把file写到outDir下面
targetFile.text = change.file.text
}
}
}
}
IncrementalTask定义了两个属性,一个是inputDir 另外一个是outputDir,taskAction是Task被执行的时候回调的方法,这个Task很简单,接收一个inputDir参数,并且把inputDir下面的所有文件写到对应的outDir目录下面,代码里面我也给出了注释了,下面我们再实现下这个抽象的接口(Tips: Gradle里面的type就类似于java的extends,譬如我们常见的task collectApks(type: Copy) 可以理解为继承下Copy接口,然后重写了from to来实现一些拷贝的功能)
task fileIncrementalTask(type: IncrementalTask) {
inputDir = file('inputs')
outputDir = file("$buildDir/outputs")
}
我们的workspace目录结构大致如下
├── build.gradle
└── inputs
├── 1.txt
├── 2.txt
└── 3.txt
最后我们执行这个task看下输出结果
[nls:gradle nls$ gradle -q fileIncrementalTask
Executing non-incrementally
ADDED: 1.txt
ADDED: 2.txt
ADDED: 3.txt
[nls:gradle nls$
第一行打印出了Executing non-incrementally了,InputChanges的incremental属性是个boolean类型,当为true的时候可以理解为有文件被修改了,当为false的时候调用inputChanges.getFileChanges,所有文件会被告知为ADDED(文件不一定真的被ADDED进来 这里可以简单理解为没有发生增量,所有文件均被视为有被修改)更多关于incremental属性的说明可以参考官方文档incremental 。接着后面的inputChanges.getFileChanges是用来获取哪些文件有被修改过的,1.txt 2.txt 3.txt 都是新增加的文件,被打印出来了,然后我们再次执行下Task看下输出结果
[nls:gradle nls$ gradle fileIncrementalTask
BUILD SUCCESSFUL in 0s
1 actionable task: 1 up-to-date
[nls:gradle nls$
这次的执行没有任何的输出了,因为我们没有修改过任何的文件,我们尝试着修改下1.txt 的文件内容然后再次执行Task
[nls:gradle nls$ gradle -q fileIncrementalTask
Executing incrementally
MODIFIED: 1.txt
[nls:gradle nls$
可以看见这次的执行,Gradle检测出来了 1.txt文件内容被修改了,现在我们尝试着增加一个 4.txt 文件 然后再执行下Task
[nls:gradle nls$ gradle -q fileIncrementalTask
Executing incrementally
ADDED: 4.txt
[nls:gradle nls$
增加的4.txt文件也被检测出来了,现在再试下删除掉2.txt 文件然后再执行下Task
[nls:gradle nls$ gradle -q fileIncrementalTask
Executing incrementally
REMOVED: 2.txt
[nls:gradle nls$
同样的2.txt 文件的删除操作也是可以被检测到了
总结下:首次进行增量Task构建的时候,肯定会先进行一次全量的构建(因为Gradle需要把状态记录下来,首次构建的时候由于没有前一次的构建记录所以肯定会执行全量的构建),再次执行构建的时候Gradle没检测到任何文件被修改的记录,所以没有任何的输出了,当我们修改了文件的内容的时候,被修改的文件就会被检测出来,同样的当我们增加或者删除一个文件的时候,增删的文件照样是可以被Gradle检测出来,Gradle内部就是通过这种方式去检测构建项目的增删修改文件的。
理想情况下,所有增删修改都能正常被检测出来,那么有没有非正常的情况吗?能这样提问的当然是有的了,下面再说说几种会影响到增量构建的情况。
- 没有前一次的构建历史记录(上面已经提到过了)
- 前一次的构建跟这次的构建Gradle的版本不一致
- upToDateWhen 被设置为false了
- 跟上一次构建对比,某个输入属性发生了变化
- 跟上一次构建对比,某个non-incremental输入属性发生了变化
- 一个或者多个输出文件发生了变化
有发生以上的这些情况,调用inputChanges.getFileChanges时,Gradle把所有input file标记为ADDED并且返回(就是说增量失效的意思了)。下面我们一一来验证下。(1、2、4这两种情况就不去验证了)
跟上一次构建对比,某个non-incremental输入属性发生了变化
还是用上面的例子,我们增加一个non-incremental的属性inputDir1,如下
abstract class IncrementalTask extends DefaultTask {
@Incremental
@PathSensitive(PathSensitivity.NAME_ONLY)
@InputDirectory
abstract DirectoryProperty getInputDir()
@InputDirectory
abstract DirectoryProperty getInputDir1()
//省略下面的代码了
}
task fileIncrementalTask(type: IncrementalTask) {
inputDir = file('inputs')
inputDir1 = file('inputs1')
outputDir = file("$buildDir/outputs")
}
然后再增加一个inputs1目录,结构如下
├── build.gradle
└── inputs
├── 1.txt
├── 2.txt
└── 3.txt
└── inputs1
├── 1.txt
我们先执行一遍Task,再次执行的时候Gradle没有任何输出,因为没有检查到任何修改内容
[nls:gradle nls$ gradle fileIncrementalTask
BUILD SUCCESSFUL in 0s
1 actionable task: 1 up-to-date
[nls:gradle nls$
当我们每次修改inputs1下面的1.txt 的时候,再去执行下Task
[nls:gradle nls$ gradle -q fileIncrementalTask
Executing non-incrementally
ADDED: 1.txt
ADDED: 3.txt
ADDED: 4.txt
[nls:gradle nls$
可以清楚得看得到,虽然这里我们的inputs1参数只是作为传参,没有参与Task的任何逻辑处理或运算,但是一旦inputs1的内容被修改了,将会影响到整个Task的Incremental判断。
upToDateWhen 被设置为false了
我们把task的upToDateWhen强制设置为false,然后多次执行Task观察下结果,如下
task fileIncrementalTask(type: IncrementalTask) {
inputDir = file('inputs')
inputDir1 = file('inputs1')
outputDir = file("$buildDir/outputs")
outputs.upToDateWhen {
false
}
}
[nls:gradle nls$ gradle -q fileIncrementalTask
Executing non-incrementally
ADDED: 1.txt
ADDED: 3.txt
ADDED: 4.txt
[nls:gradle nls$ gradle -q fileIncrementalTask
Executing non-incrementally
ADDED: 1.txt
ADDED: 3.txt
ADDED: 4.txt
[nls:gradle nls$
可以看到,不管妳执行多少次Task,Incremental都是失效的。
一个或者多个输出文件发生了变化
这个就比较简单了,我们只要修改下outputDir下面的文件内容,再执行下Task
[nls:gradle nls$ gradle fileIncrementalTask
BUILD SUCCESSFUL in 0s
1 actionable task: 1 up-to-date
[nls:gradle nls$ gradle -q fileIncrementalTask
Executing non-incrementally
ADDED: 1.txt
ADDED: 3.txt
ADDED: 4.txt
[nls:gradle nls$
当我们增加或者删除outputDir下面的文件时,也是会影响到Incremental的正常执行,但是增加一个文件的时候不会受到影响
章结:本章节介绍了Gradle的文件增删修改检测实现以及用法(其原理部分较为复杂,后面我会单独开文章来讲解),Gradle内部也是通过这种方式了检测构建项目中代码文件的变化状态情况,下一节Gradle增量编译系列第三课,我讲会从代码层出发,给大家讲解下,Gradle在检测到增量文件时,又是做了哪些处理,Gradle又是如何去检索项目中代码文件的依赖关系的,下节再见。