你是否对gradle怎么处理task间的依靠感到好奇,创立task的办法有许多种,树立依靠的办法也许多,gradle是怎么确认终究task的履行次序的,下面咱们就来探求一下

先用一张图来展现task相关的概念

Gradle深入解析 - Task原理(Graph篇)

Creation

Task的创立

先来张图协助了解

Gradle深入解析 - Task原理(Graph篇)

task的创立首要可以分为2种办法

  1. create
  2. register

create会当即创立task
register只是注册了一个task provider(后面再解说这个概念),此刻并没有当即创立task实例,这也是官方现在推荐的task创立办法,官方plugin中task的创立办法都已修正为了register

task会在build script脚本或许plugins中,经过调用tasks.create/regster的办法被增加到task container

tasks.create('hello') {
    doLast {
        println 'greeting'
    }
}
tasks.register('hello') {
    doLast {
        println 'greeting'
    }
}

TaskContainer

咱们知道gradle对每个Project都会创立一个Project目标与之相关,且咱们在build script运用到的Task相关的办法,都会被定向到Project目标上来,而Project目标关于Task的处理都是托付给TaskContainer的,可以简略的将它了解为一个寄存Task的容器

Gradle深入解析 - Task原理(Graph篇)

从2者的签名可以看出,createconfigureClosure是Closure类型,这个Closure是groovy.lang.Closure,而register是Action,2者并存,是因为早期重度运用groovy导致,前者会经过ConfigureUtil.configureUsing(configureClosure) 将closure转为action

TaskContainer可以简略分为2部分,一个map,一个是pendingMapcreate创立的task是增加到map中的,register注册的task provider放在pendingMap中,pendingMap中的task provider,在其task被创立时会自动增加到map中,并从pendingMap中移除自己

终究的task实例是经过反射创立的,假如没有指定其Task类型,那么默许会生成DefautTask的类型,可以在create/register时传入结构器参数,也可以经过configure action的办法传参

懒加载

create和register的差异

简略的可以了解为create对task的创立是eager的,而register是懒加载
gradle履行有3个阶段,initialization、configuration、execution,而不管履行哪个Task,configuration阶段都是必定存在的,在这一阶段会履行build script
假如是create办法,那Task就会被当即创立,这其实隐含了一个问题–被创立的Task或许并不会被运转,例如在咱们想要运转compileJava这个task,build scripteval进程中,将test相关的Task也都创立了
运用register就可以躲避这个问题,Task并没有当即创立,而是在需求的时分创立

这儿你或许还会有疑问,尽管create创立了Task,可是register也是会创立Task Provider的呀,而大部分Task在其结构器中或许并没有额外操作,register有好在哪呢?

其实register比较于create,不仅是Task本身创立的机遇推迟,还表现在对configuration action的履行机遇上,create在创立完Task后是会当即对其进行configure的,而register办法注册的Task,是在其需求时才被创立,也在那时才进行configure

官方称为task configuration avoidance,用以躲避不必要的Task的创立、装备
例如运用register代替create
运用named代替getByName等等

理想的task创立时间是在Task Graph calculation期间,build scan供给了可视化的数据协助定位过早创立task的问题

可以参阅官方文档task_configuration_avoidance

Lazy Properties

除了Task本身创立的lazy化外,Task的property也是可以lazy的,Task特点的lazy化首要处理的问题是,在对Task进行装备时,有些特点不必定能马上得它的值,它或许要经过杂乱的核算或许是依靠其他Task运转的成果,跟着构建杂乱性的增加,手动维护这些依靠联系会变得杂乱,而将这些特点lazy化后,不马上求值,等到需求的时分再去评价其值,来下降构建脚本的维护成本

Lazy Properties可以经过2种类型进行装备

  • Provider
  • Property

差异在于Property是可变的,Provider值是不可变的。Property实践上是Provider的子类
register办法回来的Task Provider正是Provider的子类

Property有get/set办法设置和获取值
Provider只能get获取值

特点也可以经过Extension设置

interface CompileExtension {
    Property<String> getClasspath()
}
abstract class Compile extends DefaultTask {  
    @Input  
    abstract Property<String> getJdkVersion()
    @Input  
    abstract Property<String> getClasspath()
}  
project.extensions.create('compile', CompileExtension)
def a = tasks.register('a', Compile) {  
    classpath = compile.classpath
    jdkVersion = '11'
    doLast {  
        println classpath.get()
        println jdkVersion.get()
    }
}
compile {  
    classpath = 'src/main/java'  
}

./gradlew a
输出
src/main/java
11

Property泛型不是对一切类型都能运用,filescollections比较特别,有独自的Property
关于文件file和directory还有区分

RegularFileProperty
DirectoryProperty

ListProperty
SetProperty
MapProperty

关于特点假如运用错误,gradle会有报错提示,例如给RegularFileProperty设置了文件目录,或许文件不存在,都会有相应的报错提示

Property有必要用input/output注解符号(例如上面代码中的@Input),不然会报错,Property和task依靠,task up-to-date检查都有联系,下面在依靠联系处理中会介绍inputs/outputs

Property不必手动进行初始化,上面的例子中可以看出都是abstract的,gradle在创立task实例时会默许去创立好,咱们在运用时只需考虑赋值,并且在装备时有必要赋值不然会报错,或许标示 @Optional来标明此Property非有必要

更多内容请参阅官方文档lazy_configuration

NamedDomainObjectCollection

TaskContainer完结了NamedDomainObjectCollection接口,这个概念需求提一下,gradle中有许多东西用到
例如tasksextensions实践都是NamedDomainObjectCollection
可以直观地从姓名来了解它
Named 签字的
Domain 用于某一域的
ObjectCollection 目标调集

NamedDomainObjectCollection完结了java的调集Collection接口
因为它的签字特点,实践上可以简略地将其简略地看作一个Map,实践终究的逻辑也确实是交给map处理的
它还有一个namer办法需求重写,这个作用便是用来给增加进来的元素进行命名的

Task Graph

全体流程

build script履行完之后,Task的创立和注册也就完结了,一切的Task都被增加到了Project的TaskContainer中,之后便是构建一切要履行的Task的有向无环图了,这个图是以咱们在运转gradle指令时输入的entry tasks为起点开端构建起来的,例如./gradlew build中的buildentry task可以存在多个

ExecutionPlan是寄存Task的容器,一切的Task都会被增加到中,在entry tasks被增加进来之后,会触发对Task依靠的探究,循环履行直到一切的Task依靠联系都清楚

之后求到entry tasks的拓扑排序,确认终究的履行计划

这儿包含了2个大体的作业

  1. task依靠的resolve
  2. task履行次序的确认

以下图举例,在履行./gradlew D

Gradle深入解析 - Task原理(Graph篇)

以D作为entry task
D依靠C
C依靠B和A
B依靠A

整个履行流程便是A -> B -> C -> D这样的次序

Task Relationship

在说详细的依靠处理前,咱们先需求了解有多少种树立依靠联系的办法

Task之间有以下几种办法树立相关的办法

task inputs依靠
dependsOn
finalizedBy
mustRunAfter
shoulRunAfter

dependsOn是最常见的,这儿就不说了,简略介绍下其他的办法

Task inputs

  • property办法
abstract class A extends DefaultTask {
    @OutputFile
    abstract RegularFileProperty getOutputFile()
}
def a = tasks.register('a', A) {
	outputFile = layout.buildDirectory.file('build/a')
}
tasks.register('b') {
	inputs.property('a.outputFile', a.flatMap { it.outputFile })
	doLast {
		println inputs.properties['a.outputFile']
	}
}

task b经过property和task a树立依靠联系

  • files办法
def a = tasks.register('a') {
	outputs.files('build/a')
}
tasks.register('b') {
	inputs.files(a)
}

task b的inputs和task a的outputs树立了依靠联系

finalizedBy

finalizedBy望文生义,会把依靠的Task放在entry task之后履行, 例如

def c = tasks.regsiter('c')
tasks.regsiter('d') {
	finalizedBy c
}

履行./gradlew d,会先履行d,然后履行c

mustRunAfter/shouldRunAfter

mustRunAftershouldRunAfter比较于其他几种偏弱,实践上并不是依靠,而是设置履行次序,这2种办法引入的task依靠,假如在task graph中没有的话是不会被履行的

def c = tasks.regsiter('c')
tasks.regsiter('d') {
	mustRunAfter c
}

例如履行./gradlew d指令,只履行d task,c 不会履行 履行./gradlew d c指令,会先履行c,再履行d

mustRunAfter/shouldRunAfter只是用来设置task履行的优先级,并不会给task增加强依靠 shouldRunAfter比较mustRunAfter更弱一些,履行的优先级不必定可以彻底保证,例如在parallel形式下或许task有因它而成环的问题时

每种relationship都有自己对应的TaskDependencyTaskDependency本质上是一个寄存依靠的容器。调用上面对应的办法,便是在往对应的容器中增加元素,同一容器内保存依靠的次序是依照其name的排序来的

依靠的类型没有限制,例如dependsOn字符串(Task的name),create的Task实例,registerTask Provider实例都可以,也便是说TaskDependency这个容器内寄存的元素成分很杂乱,接下来看看gradle怎么resolve这些依靠

Task Dependency Resolve

ExecutionPlan

ExecutionPlan是用来处理整个Task Graph的入口,Task依靠resolve及履行拓扑序的确认都是由这处理的

先以一张全体的流程图来协助了解

Gradle深入解析 - Task原理(Graph篇)

entry tasks被增加到ExecutionPlan后则会触发对task依靠的探究,对应于 DefaultExecutionPlandiscoverNodeRelationships

DefaultExecutionPlan
以下代码有修正,这儿保留了大体逻辑

public void addEntryTasks(Collection<? extends Task> tasks) {
	LinkedList<Node> queue = new LinkedList<>(tasks);
	discoverNodeRelationships(queue);
}
private void discoverNodeRelationships(LinkedList<Node> queue) {
	Set<Node> visiting = new HashSet<>();
	while (!queue.isEmpty()) {
		Node node = queue.getFirst();
		if (visiting.add(node)) {
			node.resolveDependencies(dependencyResolver);
			for (Node successor : node.getDependencySuccessors()) {  
			    if (!visiting.contains(successor)) {  
			        queue.addFirst(successor);  
			    }
			}
		} else {
			queue.removeFirst();  
			visiting.remove(node);
			for (Node finalizer : node.getFinalizers()) {  
			    finalizers.add(finalizer);  
			    if (!visiting.contains(finalizer)) {  
			        queue.addFirst(finalizer);  
			    }  
			}
		}
	}
}

全体上是一个DFS,node的DependencySuccessors是上面介绍过的Task RelationshipinputsdependsOn树立的依靠。在node的依靠悉数处理完后,会将它的finalizer task增加到自己后边

Task的依靠联系保存在多个TaskDependency中,关于Task依靠的resolve便是去遍历这些TaskDependency,代码逻辑入口处是在LocalTaskNode中的,也便是由entry task开端,将整个依靠联系进行处理,见下图(有删减)

LocalTaskNode是一个封装了taskNodeNode有多种类型,这儿的算法是可以针对一切类型的Node

Gradle深入解析 - Task原理(Graph篇)

对Task依靠的resolve是经过TaskDependencyResolver来完结的,而TaskDependencyResolver对依靠的处理终究是交给CachingDirectedGraphWalker来处理的

CachingDirectedGraphWalker

里边运用的是tarjan强连通图算法的变体,它有2个功用

  • findValues 查找从start node可达的nodes
  • findCycles 查找图中存在的环

了解强连通图算法Tarjan’s strongly connected components algorithm – Wikipedia的同学应该知道它可以用来查找图中的环,强连通的概念本身便是节点间俩俩都能互达,而在有向无环图中是不或许存在的,所以是对算法进行了修正,以便可以找到依靠节点
更多关于强连通图算法的知识咱们可以自行查找了解,这儿不做更多阐明了。

这儿现在是用findValues去寻找依靠的节点,实践上这儿并不是把Task的依靠及其直接依靠彻底确认下来,只是将start node的直接依靠确认下来。
还是以上图举例,从D动身只是先找到C,然后C只找到BAB找到A
并非是这个类才能缺失导致不能一次将一切依靠都查找完,这儿是因为graph给出node的办法导致的。不确认是否是成心如此规划的,可是会发生大量的中心节点,配合缓存导致空间的糟蹋

别的从姓名中的Caching可以看出它是带有缓存功用的,也便是探究过的node,下次再探究到的时分可以直接复用缓存成果

CachingDirectedGraphWalker在查找的进程中会调用graph.getNodeValues去获取节点,

Gradle深入解析 - Task原理(Graph篇)

getNodeValues有3个参数,node是当时节点,values是node对应的值,connectedNodes是相关的节点,例如task d依靠于task c的话,那么task c便是task dconnectedNodes

TaskGraphImpl完结了DirectedGraph接口,它首要担任2件事情

  1. 调用DefaultTaskDependency.visitDependencies去resolve task的依靠
  2. 调用WorkDependencyResolverTask 转化为LocalTaskNode

这一步当时的意图是为了将Task的依靠图Graph厘清,并没有确认其履行次序

依靠resolve

visitDependencies

这儿用到了Visitor规划形式,许多目标完结了TaskDependencyContainer接口,并且大多都是作为容器运用,运用Visitor形式的好处便是可以不修正这些类的完结来增加功用,Visitor对这些类进行遍历拜访后,逻辑在自己内部处理

Task依靠可以有许多种类型,这儿剖析几种首要的状况

  • Task

依靠create办法创立的Task

def a = tasks.create('a')
tasks.register('b') {
	dependsOn a
}
  • Provider

依靠register办法创立的Task,register的Task会回来Task Provider目标

def a = tasks.register('a')
tasks.register('b') {
	dependsOn a
}
  • TaskDependencyContainer

inputs的引入的依靠

这儿需求先了解一下inputs概念

input analysis

概念

一般来说,Task都会有inputsoutputsinputs可以有文件或许特点,而outputs便是文件

task将输入输出特点的界说首要分为4个类别

  • Simple values
    基本类型,字符串等完结了Serializable的类型
  • Filesystem types
    File,或许用Project.file()等gradle文件操作生成的目标
  • Dependency resolution results
    依靠裁决的成果,实质上也是文件
  • Nested values
    以上类型的嵌套组合

compileJava task为例,在编译java代码时inputs可以有许多,例如source filestarget jvm version,还可以指定编译时可用最大内存,outputs便是class文件

自界说Task的特点有必要用注解标示,假如没有标示的话,运转时会报错。 这儿的特点是指JavaBeans的带有getter/setter办法的public字段,和上面说到的用于lazy configuration的Property纷歧样

Task的特点剖析会解析父类的,有些办法例如承继自DefaultTask或许Object的办法不会被解析

作用

符号上注解有2个首要的作用

  1. inputs/outputs相关的依靠剖析
  2. Incremental Buildup-to-date check
怎么给特点标示注解

gradle供给的注解有许多

Input 用以标示一个普通类型
InputFiles 用以标示是一个输入的文件相关类型
Nested 用以标示潜套类型
OutputFiles 用以标示是一个输出的文件相关类型
Internal 用以标示一个特点是内部运用

等等,详细参阅task_input_output_annotations
@Internal这个注解值得多说一句
例如上面说到的编译时可用最大内存。source filestarget jvm version的改变都会影响到class文件的编译成果,可是运转时可用最大内存对编译成果无影响。这种和输入输出无关的特点,对Incremental Build缓存成果不发生影响的成果,可以用这个进行标示
这也标明@Input@InputFiles等这些注解标示的特点是对缓存成果有影响的

例如

class SimpleTask extends DefaultTask {
    @Input String inputString 
    @InputFiles File inputFiles  
    @OutputFiles Set<File> outputFiles   
    @Internal Object internal  
}

inputs/outputs有2个来历

  1. 经过给特点加注解的办法
  2. 调用inputs的api增加

例如

abstract class Compile extends DefaultTask {
    @Input  
    abstract Property<String> getClasspath()  
}
tasks.register('compile', Compile) {
	classpath = 'src/main'// 1. 特点注解办法
	inputs.property('name', 'compile')// 2. inputs增加特点
	inputs.files(project.files('libs'))// 3. inputs增加文件
}

2者不同之处在于,注解办法才能更强,inputs api是注解办法的子集,它可以供给@Input@InputFiles等注解的部分才能,可是其他的注解类似@Internal等它没有对应的办法
供给inputs的意图是咱们在创立三方库供给的Task时,可以简略的供给一些额外参数,而不必经过承继的办法,在界说自己的Task时,注解办法还是首选
以下将注解办法标示的特点称为AnnotatedPropertiesinputs参加的特点称为RegisteredProperties

gradle怎么剖析inputs树立的依靠

详细履行逻辑是由PropertyWalker处理的,关于每个特点的处理,也运用到了Visitor形式

Gradle深入解析 - Task原理(Graph篇)

来历有2种,所以对不同的来历都要进行剖析

AnnotatedProperties

要剖析注解的特点,首要要把注解的特点都解析出来,gradle把解析出来的数据封装为metadata,保存有特点的名称,所标示的注解的类型,以及Method本身
这儿一起会对特点进行有效性校验,每种注解都有对应的annotation handler去处理,一切的handler都保存在map中,经过annotation的类型去获取。例如@InputFiles会校验特点回来值为文件相关类型,假如是其他类型会进行报错
注解的特点解析完后会对每个特点进行遍历,对其进行visit,每种注解的处理办法也不尽相同,所以也是交给handler去处理的,关于inputs来说首要分为2种,一种是普通的特点,一种是文件特点,对应上面的PropertyVisitor的2个办法

RegisteredProperties

经过inputs api办法增加的特点会根据本身状况被参加到2个容器中,一个用于寄存文件相关类型的,一个用于寄存其他类型的,在visitor剖析时会对2者别离进行

不同的Task之间又是怎么经过这些特点树立的相关呢,让咱们从一个详细的例子下手

def e = tasks.register('e', CustomTask) {
    inputs.property('prop1', a.flatMap { it.outputFile })  
    inputs.files(b)  
    prop2 = c.flatMap { it.outputFile }  
    prop3 = d.files  
}

上面截取了部分代码,总共有5个Task,task etask a,b,c,d都有依靠联系。a,b,c都是register的Task,d是create的Task

  • prop1经过inputs.property的办法依靠task aa.flatMap回来的是Provider保存了task a的信息,task a本身也是Provider,gradle经过反射调用Task特点的getter的办法可以拿到task a,将其作为依靠
  • inputs.files直接依靠了task b,inputs.files(b)实践上是对task b的outputs文件的依靠,和FileCollection处理一致
  • prop2依靠了task c,处理办法同prop1
  • prop3依靠了task dd.files回来的是FileCollection,在创立时也保存了task d的信息

因为可以作为依靠增加的目标许多,差别也很大,所以gradle运用了visitor形式,详细的目标在visit办法中处理自己的依靠办法,最终visitor将一切的依靠进行收集

关于详细特点剖析的逻辑终究收拢到了PropertyVisitor中,TaskInputs会将这些依靠增加到connectedNodes,让图的查找作业继续进行

这儿只对inputs相关做了阐明,实践特点的处理还有与增量构建相关的逻辑,在之后缓存的文章中再进行阐明

Task的依靠resolve完后,依靠会被保存在多个容器中,dependencyNodesdependentNodes别离标明此Task依靠的Task和依靠此Task的Task,mustRunAftershouldRunAfter等也会有独立的容器寄存

Project依靠导致的Task依靠

inputs依靠办法还有一种特别的状况,便是project间的依靠联系 假设有2个project,libAlibBlibB依靠libA

libA/build.gradle

plugins {
    id 'java'  
}

libB/build.gradle

plugins {
    id 'java'  
}
dependencies {
	implementation(project(':libA'))
}

经过dependencies的办法2者就树立了依靠联系,在履行./gradlew libB:compileJava时会先履行libA:jar task,这又是怎么做到的呢?

也便是说因为implementation(project(':libA'))的联系,libB:compileJavalibA:jar发生了依靠

libA applyjava pluginjava plugin中将PublishArtifactJar task树立相关,并将 PublishArtifact 作为 libA Configuration 的一部分
简略地了解便是libA的输出产物是PublishArtifact,而PublishArtifact是由Jar task生成的 (Configuration是gradle Dependency的一个概念,之后在依靠处理中详细阐明,这儿将它简略了解为一堆文件就可以了)

CompileJava task有一个特点classpathlibB compileJava时,classpath经过project(':libA')libA发生了依靠,classpathCompileJava task inputs的一部分,它对应的也是一堆文件,有一部分是来自于libA的输出产物
在处理Task的依靠时,经过Configuration查找到了libAPublishArtifact,之后顺理成章地和libAJar task树立了依靠联系,本质上也是经过TaskInput处理的依靠联系

履行次序

Task的依靠联系图即 Task Graph,正常状况下是一个有向无环图(DAG),在它被resolve之后,此刻就可以开端对Task Graph进行拓扑序的求解了,得到最终履行的次序

拓扑排(Topological Order) 实质上是将DAG图的顶点依照其指向联系排成一个线性序列

假如graph有环,那拓扑序求解会失败,这个时分会调用CachingDirectedGraphWalker,也便是运用tarjan强连通图算法去找环,意图是为了报错信息可以让运用者直观地看出是哪些task有相互依靠的状况,便于修正。顺带一提,强连通算法经过查找环来进行报错信息优化,在代码编译中也有许多运用场景,例如假如把正常的承继联系看作一个有向无环图,那么循环承继这种状况就可以运用这种算法找到是哪些类的发生了循环承继

求拓扑序的办法有许多,且拓扑序并不仅有,有或许有多种解,gradle运用的是DFS办法,将entry nodes作为起点增加到行列中,来进行查找,一起了用来遍历查找task的Queue,用来保存终究成果的Set,用来保存符号是否visit过的visitingNodes这几个数据结构
entry nodes或许为多个,这儿以最简略一个的状况阐明一下全体过程

  1. 判别行列是否为空
    1. 假如为空则完毕,保存成果的set的次序便是排序成果
    2. 假如不为空,取行列中第一个node
  2. node是否现已存在成果set中了,存在的话直接移除行列中的node,重复过程1
  3. node状况是否为“查找中”,假如查找过node则将其保存到成果的set中,并移除行列中的node,重复过程1,不然符号当时node
  4. node的直接依靠结点successors
    1. 假如nodesuccessors中存在状况为“查找中”,那么标明DAG图有环,进行报错提示
    2. nodesuccessors悉数增加到行列中,回到过程1判别行列是否为空

这儿的successors标明的是当时node经过上面介绍过的几种树立依靠的办法相关起来的一切Task

流程图如下

flowchart TD
	start(开端)
	finish(完毕)
	is_queue_empty{queue是否为空}
	first_node_complete{第一个node是否现已在成果中了}
	first_node_visited{第一个node是否被visit过}
	successor_visited{node的succussors存在被visit过}
	complete(将第一个node参加到成果中)
	error(报错)
	flag_visit(符号node状况为visit)
	successor_add(将node的successor加到queue中)
	remove_first_node(移除第一个node)
	start --> is_queue_empty
	is_queue_empty -->|是| finish
	is_queue_empty -->|否| first_node_complete
	first_node_complete -->|是| remove_first_node
	first_node_complete -->|否| first_node_visited
	first_node_visited -->|是| complete
	first_node_visited -->|否| flag_visit
	flag_visit --> successor_visited
	successor_visited -->|是| error
	successor_visited -->|否| successor_add
	successor_add --> start
	remove_first_node --> start

大致代码如下

void processNodeQueue() {
    while (!queue.isEmpty()) {  
        final Node node = queue.peekFirst();  
        if (result.contains(node)) {  
            queue.removeFirst();  
            visitingNodes.remove(node);  
            continue;
        }  
        if (visitingNodes.put(node)) {  
            ListIterator<Node> insertPoint = queue.listIterator();  
            for (Node successor : node.getAllSuccessors()) {  
                if (visitingNodes.containsEntry(successor)) {
                    onOrderingCycle(successor, node);  
                }  
                insertPoint.add(successor);  
            }  
        } else {  
            queue.removeFirst();  
            visitingNodes.remove(node);  
            result.add(node);  
        }  
    }  
}

以上面图示的依靠联系来举例,大概过一下全体流程

Gradle深入解析 - Task原理(Graph篇)

这儿还省略了许多细节的处理,比较重要的有以下几点

  1. finalizedBy引入的依靠,会被加到对应Task的刚好后面一个,例如 a.finalizedBy(b) c.dependsOn(a) 那么b会位于queue中ac的中心,也就保证了履行的次序
  2. 假如Task是由mustRunAfter/shouldRunAfter增加的,且没有其他强依靠的办法引用到,是不会被加到成果中的
  3. 成环的判别那里,假如是因为shouldRunAfter形成的会疏忽掉
  4. entry nodes可以是多个,处理多个entry nodes时,每个entry nodes会对应一个segment将不同的node区分开来

参阅文档

Authoring Tasks
Incremental build
Developing Custom Gradle. Task Types
Lazy Configuration
Task Configuration Avoidance
Developing Parallel Tasks using the Worker API

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。