1. Einleitung
Dieser Artikel ist der siebte Artikel der Gradle-Reihe , der Ihnen Task
die Wissenspunkte zum Kern der Gradle-Konstruktion vermittelt.
2. Was ist die Aufgabe in Gradle?
Task
ist eine Aufgabe und Gradle
die kleinste davon 构建单元
.
Der Kern der Gradle-Konstruktion ist ein gerichteter azyklischer Graph, der aus Task besteht:
Task ist das Kernobjekt der Gradle-Konstruktion. Task kann Eingaben empfangen, bestimmte Operationen ausführen und Ausgaben entsprechend den ausgeführten Operationen generieren.
Die Aufgabe verwaltet eine Action
Liste. Sie können eine Aktion (doFirst) vor der Liste einfügen oder (doLast) am Ende der Liste einfügen. Die Aktion ist die kleinste 执行单元
.
3. So erstellen Sie eine Aufgabe
3.1 Wo steht die Aufgabe?
Überlegen Sie zunächst, wo die Aufgabe geschrieben ist.
Wir haben im vierten Lebenszyklus der Gradle-Reihe eingeführt, dass es drei Phasen gibt. Die erste Phase der Initialisierung bestimmt, welche Projekte an der Kompilierung teilnehmen. Die zweite Phase besteht darin, die Konfiguration zu analysieren und die Task Registry (DAG) zu generieren. Die dritte Phase Die Phase besteht darin, die Aufgabe nacheinander auszuführen.
Umgekehrt erfordert die Ausführung einer Aufgabe eine Aufgabenregistrierung. Die Quelle der Aufgabe muss bestimmen, welche Projekte an der Kompilierung teilnehmen. Das heißt, die Aufgabenregistrierung wird durch die an der Kompilierung beteiligten Projekte bestimmt. Es kann verstanden werden Da die Aufgabe durch das Objekt bestimmt wird, Project
wird die Aufgabe also im Projekt erstellt. Eine build.gradle-Datei entspricht einem Projektobjekt, sodass wir die Aufgabe direkt in der build.gradle-Datei erstellen können.
Solange sich der laufende Kontext im Project befindet .
3.2. Aufgabe erstellen
Um eine Aufgabe zu erstellen, müssen Sie die Registermethode von TaskContainer verwenden.
Mehrere Möglichkeiten zur Registrierung:
- register(String name, Action<? super Task> ConfigurationAction)
- register(String-Name, Klassentyp, Action<? super T> ConfigurationAction)
- register(Stringname, Klassentyp)
- register(String-Name, Klassentyp, Objekt… KonstruktorArgs)
- register(String-Name)
1 und 2 werden häufiger verwendet.
- „configurationAction“ bezieht sich auf die Aktion, also auf den Vorgang der Aufgabe, der zur Kompilierungszeit ausgeführt wird.
- Der Typtyp bezieht sich auf den Aufgabentyp, der ein benutzerdefinierter Typ sein kann, oder Sie können die integrierten Typen Kopieren, Löschen, Zip, Jar und andere angeben.
Wir können eine Aufgabe direkt in der Datei build.gradle erstellen:
tasks.register("yechaoa") {
println("Task Name = " + it.name)
}
Der obige Aufruf ist die Methode 1 des Registers. Wenn der letzte Parameter ein Abschluss ist, kann er außerhalb des Parameters geschrieben werden.
Wir haben die obige Aufgabe über TaskContainer (Aufgaben) erstellt. Die Methode zum Erstellen einer Aufgabe wird auch im Projektobjekt bereitgestellt. Es gibt einen kleinen Unterschied in der Schreibweise:
task("yechaoa") {
println "aaa"
}
Aufgaben können auch über das Plugin erstellt werden, und die neu geschriebene Apply-Methode verfügt über ein Project-Objekt.
4. So führen Sie eine Aufgabe aus
4.1, führen Sie eine einzelne Aufgabe aus
Befehl:
./gradlew taskname
Beispiel:
tasks.register("yechaoa") {
println("Task Name = " + it.name)
}
implementieren:
./gradlew yechaoa
Ausgang:
Task Name = yechaoa
4.2, Mehrere Aufgaben ausführen
./gradlew taskname taskname taskname
Aufgaben werden durch Leerzeichen getrennt.
4.3, gleichnamige Aufgabe
Wenn zwei Aufgaben mit demselben Namen vorhanden sind, schlägt die Kompilierung fehl, d. h. InvalidUserDataException
* What went wrong:
A problem occurred evaluating project ':app'.
> Cannot add task 'yechaoa' as a task with that name already exists.
4.4, Ergebnisse der Aufgabenausführung
Zur Kompilierungszeit sehen wir oft eine Beschriftung hinter der Aufgabe, die das Ausführungsergebnis der Aufgabe angibt.
> Task :app:createDebugVariantModel UP-TO-DATE
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
> Task :app:compileDebugAidl NO-SOURCE
> Task :app:compileDebugRenderscript NO-SOURCE
> Task :app:generateDebugBuildConfig UP-TO-DATE
.....
4.4.1、EXCUTED
Zeigt die allgemeine Aufgabenausführung an.
4.4.2, AKTUELL
> Task :app:preBuild UP-TO-DATE
Es zeigt an, dass sich die Ausgabe der Aufgabe nicht geändert hat.
Es gibt mehrere Situationen:
- Weder Eingabe noch Ausgabe haben sich geändert;
- Die Ausgabe hat sich nicht geändert;
- Die Aufgabe hat keine Operation und Abhängigkeiten, aber der Inhalt der Abhängigkeiten ist der neueste oder wird übersprungen oder wiederverwendet.
- Die Aufgabe hat keine Operationen und keine Abhängigkeiten;
4.4.3、FOME-CACHE
Wörtlich bedeutet dies, dass das letzte Ausführungsergebnis aus dem Cache wiederverwendet werden kann.
4.4.4、ÜBERSPRINGT
Wörtlich bedeutet es überspringen.
Ausgeschlossen ist zum Beispiel:
$ gradle dist --exclude-task yechaoa
4.4.5、KEINE QUELLE
Aufgabe muss nicht ausgeführt werden. Es gibt Ein- und Ausgänge, aber keine Quellen.
5, Aufgabenaktion
5.1、Aktion
Die Aktion einer Aufgabe ist die zur Kompilierungszeit erforderliche Operation. Sie ist keine einzelne, sondern eine Gruppe, das heißt, es können mehrere vorhanden sein.
Bei der Anpassung werden im Allgemeinen mehrere Aufgaben verwendet.
5.1.1, Benutzerdefinierte Aufgabe
class YechaoaTask extends DefaultTask {
@Internal
def taskName = "default"
@TaskAction
def MyAction1() {
println("$taskName -- MyAction1")
}
@TaskAction
def MyAction2() {
println("$taskName -- MyAction2")
}
}
- Passen Sie eine Klasse an, die von erbt
DefaultTask
. - Die Action-Methode muss
@TaskAction
mit Anmerkungen versehen werden. - Für extern verfügbar gemachte Parameter müssen
@Internal
Anmerkungen verwendet werden.
Verwenden Sie eine benutzerdefinierte Aufgabe:
tasks.register("yechaoa", YechaoaTask) {
taskName = "我是传入的Task Name "
}
Der Typ wird an die benutzerdefinierte Task-Klasse übergeben.
Ergebnisse der:
> Task :app:yechaoa
我是传入的Task Name -- MyAction1
我是传入的Task Name -- MyAction2
Wenn der Konstruktor der Action-Methode Parameter übergibt, können die Parameter nach dem Typtyp geschrieben werden:
tasks.register('yechaoa', YechaoaTask, 'xxx')
5.2、doFirst
Es ist eine Art Aktion und wird an der Spitze der Aufgabenaktion ausgeführt. Es kann mehr als eine geben.
5.3、doLast
Es ist eine Art Aktion und wird am Ende der Aufgabenaktion ausgeführt. Es kann mehr als eine geben.
Beispielcode:
tasks.register("yechaoa") {
it.doFirst {
println("${it.name} = doFirst 111")
}
it.doFirst {
println("${it.name} = doFirst 222")
}
println("Task Name = " + it.name)
it.doLast {
println("${it.name} = doLast 111")
}
it.doLast {
println("${it.name} = doLast 222")
}
}
Ergebnisse der:
Task Name = yechaoa
> Task :app:yechaoa
yechaoa = doFirst 222
yechaoa = doFirst 111
yechaoa = doLast 111
yechaoa = doLast 222
Die Ausgabe des Aufgabennamens erfolgt in der Konfigurationsphase des Gradle-Lebenszyklus, da sie geschlossen ist, nicht in einer Aktion enthalten ist und keine Ausführungsmöglichkeit besteht. Wenn die Konfigurationsphase zu dieser Aufgabe aufgelöst wird, wird println ausgeführt.
Andere Ausgaben befinden sich unter Task :app:yechaoa, da es einen klaren Zeitplan für die Aktionsausführung gibt.
5.4, Aktionsausführungssequenz
Auch die Ausführungsreihenfolge der Aktion ist für die meisten Menschen ein Missverständnis. Lassen Sie uns das anhand des obigen Protokolls klären:
- doFirst: umgekehrte Reihenfolge
- Aktion: normale Reihenfolge
- doLast: positive Sequenz
6. Aufgabenattribut
Die Attribute der Aufgabe lauten wie folgt:
String TASK_NAME = "name";
String TASK_DESCRIPTION = "description";
String TASK_GROUP = "group";
String TASK_TYPE = "type";
String TASK_DEPENDS_ON = "dependsOn";
String TASK_OVERWRITE = "overwrite";
String TASK_ACTION = "action";
Die Attributkonfiguration ist, wie der Name schon sagt, einfacher zu verstehen.
Wenn Ihre IDEE über eine visuelle Gradle-Verwaltung verfügt, z. B. Android Studio, finden Sie unsere benutzerdefinierte Aufgabe im Menü des Gradle-Bedienfelds auf der rechten Seite und können sie durch Doppelklicken ausführen.
7. Aufgabenabhängigkeit
Gradle verfügt standardmäßig bereits über eine Reihe von Aufgabenerstellungsprozessen. Wenn Sie diesem Prozess eine benutzerdefinierte Aufgabe hinzufügen oder vor und nach einer integrierten Aufgabe eine Aspektprogrammierung durchführen möchten, müssen Sie die Abhängigkeiten der Aufgabe verstehen.
7.1, hängt davon ab
tasks.register("yechaoa111") {
it.configure {
dependsOn(provider {
tasks.findAll {
it.name.contains("yechaoa222")
}
})
}
it.doLast {
println("${it.name}")
}
}
tasks.register("yechaoa222") {
it.doLast {
println("${it.name}")
}
}
implementieren:
./gradlew yechaoa111
Ausgang:
> Task :app:yechaoa222
yechaoa222
> Task :app:yechaoa111
yechaoa111
Die Task yechaoa111, die dependOn definiert, wird nach der Ziel-Task yechaoa222 ausgeführt.
Im Vergleich zum oben Gesagten ist die gebräuchlichere Schreibweise von dependOn tatsächlich folgende:
def yechaoa111 = tasks.register("yechaoa111") {
it.doLast {
println("${it.name}")
}
}
def yechaoa222 = tasks.register("yechaoa222") {
it.doLast {
println("${it.name}")
}
}
yechaoa111.configure {
dependsOn yechaoa222
}
Die Aufgabe, von der dependOn abhängt, kann ein Name oder ein Pfad sein
Es kann auch ein Typtyp sein:
dependsOn tasks.withType(Copy)
Wenn es sich um eine Aufgabe eines anderen Projekts (Projekt) handelt, kann es auch:
dependsOn "project-lib:yechaoa"
7.2、finalizedBy
Fügen Sie die angegebene Finalizer-Aufgabe zur Aufgabe hinzu. Das heißt, die nächste auszuführende Aufgabe anzugeben, und dependOn gibt die vorherige an.
task taskY {
finalizedBy "taskX"
}
Hier bedeutet es, dass TaskX nach der Ausführung von TaskY ausgeführt wird.
Wenn finalizedBy durch dependOn ersetzt wird, bedeutet dies, dass taskX ausgeführt werden muss, bevor taskY ausgeführt wird.
7.3、mustRunAfter
def yechaoa111 = tasks.register("yechaoa111") {
it.doLast {
println("${it.name}")
}
}
def yechaoa222 = tasks.register("yechaoa222") {
it.doLast {
println("${it.name}")
}
}
yechaoa111.configure {
mustRunAfter yechaoa222
}
implementieren:
./gradlew yechaoa111
Ausgang:
> Task :app:yechaoa111
yechaoa111
Es ist ersichtlich, dass yechaoa222 nicht ausgeführt wurde, da wir yechaoa111 separat ausgeführt haben und wir es gemeinsam ausführen müssen, um die Reihenfolge der Abhängigkeiten anzuzeigen.
Lass es uns noch einmal gemeinsam ausführen:
./gradlew yechaoa111 yechaoa222
Ausgang:
> Task :app:yechaoa222
yechaoa222
> Task :app:yechaoa111
yechaoa111
Es ist ersichtlich, dass mustRunAfter wirksam wird und yechaoa111 nach yechaoa222 ausgeführt wird.
7.4、shouldRunAfter
yechaoa111.configure {
shouldRunAfter yechaoa222
}
ShouldRunAfter wird auf die gleiche Weise geschrieben wie MustRunAfter.
MustRunAfter ist „muss ausgeführt werden“, ShouldRunAfter ist „sollte ausgeführt werden“.
Wenn beispielsweise taskB.mustRunAfter (taskA) TaskA und TaskB gleichzeitig ausführen, muss TaskB immer nach TaskA ausgeführt werden.
Die ShouldRunAfter-Regel ist ähnlich, aber nicht gleich, da sie in beiden Fällen ignoriert wird. Erstens, wenn die Verwendung dieser Regel einen Sortierzyklus einführen würde; zweitens, wenn bei paralleler Ausführung alle Abhängigkeiten einer Aufgabe erfüllt sind, mit Ausnahme der „Sollte ausgeführt werden“-Aufgabe, wird sie unabhängig davon, ob ihre „Sollte ausgeführt“-Abhängigkeiten ausgeführt wurden, ausgeführt diese Aufgabe.
8. Aufgabe überspringen
Gradle bietet mehrere Möglichkeiten zum Überspringen von Aufgaben.
- bedingtes Überspringen
- Ausnahme überspringen
- Überspringen deaktivieren
- Timeout überspringen
8.1, bedingtes Überspringen
Gradle stellt onlyIf(Closure onlyIfClosure)
eine Methode bereit, um die Aufgabe nur dann auszuführen, wenn das Ergebnis des Abschlusses True zurückgibt.
tasks.register("skipTask") { taskObj ->
taskObj.configure {
onlyIf {
def provider = providers.gradleProperty("yechaoa")
provider.present
}
}
taskObj.doLast {
println("${it.name} is Executed")
}
}
implementieren:
./gradlew skipTask -Pyechaoa
Ausgang:
> Task :app:skipTask
skipTask is Executed
Die SkipTask wird nur ausgeführt, wenn dem Skriptbefehl Parameter hinzugefügt werden -Pyechaoa
.
Analog dazu sind die Bedingungen selbstbestimmt, solange das Ergebnis des onlyIf-Abschlusses wahr ist.
8.2, Ausnahmeüberspringen
Wenn nurWenn die Anforderungen nicht erfüllt sind, kann es auch StopExecutionException
zum Überspringen verwendet werden.
StopExecutionException ist eine Ausnahme. Wenn eine Ausnahme ausgelöst wird, werden die aktuelle Aktion und nachfolgende Aktionen übersprungen, dh die aktuelle Aufgabe wird übersprungen, um die nächste Aufgabe auszuführen.
tasks.register("skipTask") { taskObj ->
taskObj.doFirst {
println("${it.name} is Executed doFirst")
}
taskObj.doLast {
def provider = providers.gradleProperty("yechaoa")
if (provider.present) {
throw new StopExecutionException()
}
println("${it.name} is Executed doLast")
}
}
Ausgang:
> Task :app:skipTask
skipTask is Executed doFirst
Wenn dem Skriptbefehl Parameter hinzugefügt werden -Pyechaoa
, wird eine Ausnahme ausgelöst und übersprungen.
Die vorherige Aktion in der Aufgabe wird jedoch ausgeführt, z. B. doFirst im Beispiel.
8.3, Überspringen deaktivieren
Jede Aufgabe verfügt über einen enabled
Schalter: „true“ ist aktiviert, „false“ ist deaktiviert und nach der Deaktivierung wird kein Vorgang ausgeführt.
tasks.register("skipTask") { taskObj ->
taskObj.configure {
enabled = true
}
taskObj.doLast {
println("${it.name} is Executed")
}
}
8.4, Timeout-Übersprung
Die Aufgabe stellt timeout
Eigenschaften zur Begrenzung der Ausführungszeit bereit.
Wenn die Laufzeit der Aufgabe die angegebene Zeit überschreitet, wird der Thread, der die Aufgabe ausführt, unterbrochen.
Standardmäßig treten bei Aufgaben keine Zeitüberschreitungen auf.
Beispiel:
tasks.register("skipTask") { taskObj ->
taskObj.configure {
timeout = Duration.ofSeconds(10)
}
taskObj.doLast {
Thread.sleep(11 * 1000)
println("${it.name} is Executed")
}
}
Ausgang:
> Task :app:skipTask FAILED
Requesting stop of task ':app:skipTask' as it has exceeded its configured timeout of 10s.
---Gradle:buildFinished 构建结束了
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:skipTask'.
> Timeout has been exceeded
Die Ausführung ist abnormal und führt zu einer Zeitüberschreitung von 10 Sekunden, da ich in doLast 11 Sekunden lang schlafe.
9. Inkrementelle Aufgabenkonstruktion
增量构建
Wenn sich die Eingabe und Ausgabe der Aufgabe nicht ändern, wird die Ausführung der Aktion übersprungen. Wenn sich die Eingabe oder Ausgabe der Aufgabe ändert, wird nur die geänderte Eingabe oder Ausgabe in der Aktion verarbeitet, sodass eine Aufgabe erhalten bleibt, die sich nicht geändert hat Dies wird vermieden. Erstellen Sie wiederholt und verarbeiten Sie die geänderten Teile nur, wenn sich die Aufgabe ändert. Dies kann die Konstruktionseffizienz von Gradle verbessern und die Konstruktionszeit verkürzen.
Ein wichtiger Teil jedes Build-Tools ist die Möglichkeit, bereits erledigte Arbeiten zu vermeiden. Nach dem Kompilieren von Quelldateien müssen diese nicht erneut kompiliert werden, es sei denn, es ändert sich etwas, das sich auf die Ausgabe auswirkt, z. B. eine Änderung der Quelldatei oder das Löschen einer Ausgabedatei. Das Kompilieren kann viel Zeit in Anspruch nehmen. Daher kann das Überspringen nicht benötigter Schritte viel Zeit sparen.
Gradle bietet diese sofort einsatzbereite inkrementelle Build-Funktionalität. Beim Kompilieren wird Task in der Konsolenausgabe als markiert UP-TO-DATE
, was bedeutet, dass der inkrementelle Build funktioniert.
Werfen wir einen Blick darauf, wie inkrementelle Builds funktionieren und wie sichergestellt wird, dass Aufgaben inkrementelle Ausführungen unterstützen.
9.1. Eingabe und Ausgabe
Im Allgemeinen erfordern Aufgaben eine gewisse Eingabe und erzeugen eine gewisse Ausgabe. Wir können den Java-Kompilierungsprozess als Beispiel für eine Aufgabe betrachten. Die Java-Quelldatei wird als Eingabe der Aufgabe verwendet, und die generierte Klassendatei, dh das Ergebnis der Kompilierung, ist die Ausgabe der Aufgabe.
Ein wichtiges Merkmal einer Eingabe besteht darin, dass sie sich auf eine oder mehrere Ausgaben auswirkt. Wie oben gezeigt, werden je nach Inhalt der Quelldatei und der Mindestversion der Java-Laufzeitumgebung, auf der Sie den Code ausführen möchten, unterschiedliche Bytecodes generiert.
Wenn Sie eine Aufgabe schreiben, müssen Sie Gradle mitteilen, welche Aufgabeneigenschaften Eingabe und welche Ausgabe sind. Wenn sich eine Task-Eigenschaft auf eine Ausgabe auswirkt, achten Sie darauf, sie als Eingabe zu registrieren, andernfalls wird die Task als aktuell betrachtet, wenn dies nicht der Fall ist. Wenn umgekehrt die Eigenschaft keinen Einfluss auf die Ausgabe hat, registrieren Sie die Eigenschaft nicht als Eingabe, da die Aufgabe sonst möglicherweise ausgeführt wird, wenn sie nicht benötigt wird. Achten Sie auch auf nicht deterministische Aufgaben, die möglicherweise unterschiedliche Ausgaben für genau dieselbe Eingabe generieren. Diese Aufgaben sollten nicht für inkrementelle Builds konfiguriert werden, da die UP-TO-DATE-Prüfung nicht funktioniert.
Die oben genannten beiden theoretischen Absätze stammen von der offiziellen Website. Für Anfänger ist es möglicherweise etwas schwierig zu verstehen. Im Folgenden erfahren Sie, wie Sie es üben.
9.2. Zwei Formen der inkrementellen Konstruktion
- Die erste Aufgabe kann vollständig wiederverwendet werden und es gibt keine Änderung in der Ein- und Ausgabe, d. h. UP-TO-DATE;
- Der zweite Typ weist einige Änderungen auf und muss nur den geänderten Teil bearbeiten.
9.3, Fallpraxis
Szenario: Schreiben Sie eine Aufgabe, die Dateien kopiert und inkrementelle Builds unterstützt.
class CopyTask extends DefaultTask {
// 指定输入
@InputFiles
FileCollection from
// 指定输出
@OutputDirectory
Directory to
// task action 执行
@TaskAction
def execute() {
File file = from.getSingleFile()
if (file.isDirectory()) {
from.getAsFileTree().each {
copyFileToDir(it, to)
}
} else {
copyFileToDir(from, to)
}
}
/**
* 复制文件到文件夹
* @param src 要复制的文件
* @param dir 接收的文件夹
* @return
*/
private static def copyFileToDir(File src, Directory dir) {
File dest = new File("${dir.getAsFile().path}/${src.name}")
if (!dest.exists()) {
dest.createNewFile()
}
dest.withOutputStream {
it.write(new FileInputStream(src).getBytes())
}
}
}
Beim Schreiben einer Aufgabe müssen wir Anmerkungen verwenden, um die Eingabe und Ausgabe zu deklarieren. @InputXXX
bedeutet Eingabe, @OutputXXX
bedeutet Ausgabe.
- Der obige Code
from
ist unsere Eingabe, also die zu kopierende Datei; to
ist unsere Ausgabe, der zu empfangende Ordner;- Dann
execute()
ist die Methode die von der Task ausgeführte Aktion.
Mal sehen, wie man es benutzt
tasks.register("CopyTask", CopyTask) {
from = files("from")
to = layout.projectDirectory.dir("to")
}
Erstellen Sie vor der Ausführung Daten, fügen Sie einen From-Ordner im App-Verzeichnis hinzu und fügen Sie darunter eine txt1.txt-Datei hinzu
├── app
│ ├── from
│ │ └── txt1.txt
implementieren:
./gradlew CopyTask
Die Datei txt1.txt wurde in den Ordner „to“ kopiert.
Die Verzeichnisstruktur zu diesem Zeitpunkt:
├── app
│ ├── from
│ │ └── txt1.txt
│ └── to
│ └── txt1.txt
9.3.1, AKTUELL
Das gerade ausgeführte Protokoll:
➜ GradleX git:(master) ✗ ./gradlew CopyTask
...
BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
Führen Sie Folgendes erneut aus, um den inkrementellen Build zu überprüfen:
➜ GradleX git:(master) ✗ ./gradlew CopyTask
...
BUILD SUCCESSFUL in 1s
1 actionable task: 1 up-to-date
Dieses Mal können wir feststellen, dass sich das Ausführungsergebnis der Aufgabe von zu executed
geändert hat up-to-date
, was darauf hinweist, dass unser inkrementeller Build wirksam geworden ist.
Obwohl der inkrementelle Build zu diesem Zeitpunkt wirksam geworden ist, reicht der Grad der Fertigstellung nicht aus und es ist eine feinkörnigere Verarbeitung erforderlich. Schauen wir also mal.
9.3.2. Inkrementeller Build
Oben haben wir über die beiden Formen der inkrementellen Konstruktion gesprochen und diese auf den neuesten Stand gebracht. Jetzt müssen wir das Eingabe-/Ausgabeszenario simulieren 部分变化
.
Szenario: Fügen Sie basierend auf dem obigen Szenario eine txt2.txt-Datei unter dem Ordner „from“ hinzu und unterstützen Sie inkrementelle Builds.
Wenn Sie eine txt2.txt-Datei hinzufügen und den obigen Befehl erneut ausführen, werden Sie feststellen, dass die txt1.txt-Datei erneut kopiert wurde.
Dies liegt daran, dass sich unsere Eingabe geändert hat und die Aktion von CopyTask vollständig erstellt wird. Der gewünschte Effekt besteht darin, nur die Datei txt2.txt zu kopieren. Es werden nur die neu hinzugefügten oder geänderten Dateien kopiert, die unveränderten Dateien werden nicht kopiert.
Um diesen Effekt zu erzielen, muss die Action-Methode unterstützt werden 增量构建
.
Wir müssen InputChanges
der Action-Methode einen Parameter hinzufügen. Die Action-Methode mit dem InputChanges-Typparameter gibt an, dass es sich um eine inkrementelle Task-Operationsmethode handelt. Dieser Parameter teilt Gradle mit, dass die Action-Methode nur die geänderte Eingabe verarbeiten muss. Darüber hinaus ist die Task muss außerdem mindestens ein Deltadatei-Eingabeattribut verwenden @Incremental
oder angeben.@SkipWhenEmpty
class CopyTask extends DefaultTask {
// 指定增量输入属性
@Incremental
// 指定输入
@InputFiles
FileCollection from
// 指定输出
@OutputDirectory
Directory to
// task action 执行
@TaskAction
void execute(InputChanges inputChanges) {
boolean incremental = inputChanges.incremental
println("isIncremental = $incremental")
inputChanges.getFileChanges(from).each {
if (it.fileType != FileType.DIRECTORY) {
ChangeType changeType = it.changeType
String fileName = it.file.name
println("ChangeType = $changeType , ChangeFile = $fileName")
if (changeType != ChangeType.REMOVED) {
copyFileToDir(it.file, to)
}
}
}
}
/**
* 复制文件到文件夹
* @param src 要复制的文件
* @param dir 接收的文件夹
* @return
*/
static def copyFileToDir(File file, Directory dir) {
File dest = new File("${dir.getAsFile().path}/${file.name}")
if (!dest.exists()) {
dest.createNewFile()
}
dest.withOutputStream {
it.write(new FileInputStream(file).getBytes())
}
}
}
Die Änderungen sind hier in zwei Schritte unterteilt:
- Fügen Sie
@Incremental
Anmerkungen zum from-Attribut hinzu, um inkrementelle Eingabeattribute anzuzeigen. - Die Aktionsmethode „execute()“ wurde neu geschrieben und es wurden
InputChanges
Parameter hinzugefügt, um das inkrementelle Kopieren von Dateien zu unterstützen. DannChangeType
werden nur neu hinzugefügte oder geänderte Dateien entsprechend der Überprüfung der Dateien kopiert.
Mehrere Arten von ChangeType:
public enum ChangeType {
ADDED,
MODIFIED,
REMOVED
}
- HINZUGEFÜGT: Zeigt an, dass die Datei neu hinzugefügt wurde;
- GEÄNDERT: Zeigt an, dass die Datei geändert wurde;
- ENTFERNT: Zeigt an, dass die Datei gelöscht wurde;
Führen wir es zuerst aus:
./gradlew CopyTask
Ausgang:
> Task :app:CopyTask
isIncremental = false
ChangeType = ADDED , ChangeFile = txt1.txt
Führen Sie es zum ersten Mal aus und es gibt keinen inkrementellen Build. Führen Sie es erneut aus, um es zu sehen.
BUILD SUCCESSFUL in 2s
1 actionable task: 1 up-to-date
Das zweite direkte Update.
9.3.3, HINZUGEFÜGT
Zu diesem Zeitpunkt haben wir den inkrementellen Build noch nicht überprüft. Wir fügen eine txt2.txt-Datei zum Ordner „from“ hinzu und führen sie dann aus
> Task :app:CopyTask
isIncremental = true
ChangeType = ADDED , ChangeFile = txt2.txt
Aus dem Protokoll ist ersichtlich, dass die inkrementelle Erstellung der Aktion wirksam geworden ist. Es zeigt an, dass die Datei txt2.txt neu hinzugefügt wurde und die Datei txt1.txt nicht erneut kopiert wurde.
Die Verzeichnisstruktur zu diesem Zeitpunkt:
├── app
│ ├── build.gradle
│ ├── from
│ │ └── txt1.txt
│ └── txt2.txt
│ └── to
│ ├── txt1.txt
│ └── txt2.txt
9.3.4、GEÄNDERT
Wir können die Änderung weiter überprüfen, eine Zeile „yechaoa“ in die Datei txt1.txt einfügen, um die Änderung zu simulieren, und sie erneut ausführen
> Task :app:CopyTask
isIncremental = true
ChangeType = MODIFIED , ChangeFile = txt1.txt
Es handelt sich immer noch um einen inkrementellen Build und zeigt an, dass die Datei txt1.txt geändert und die Datei txt2.txt nicht erneut kopiert wird.
9.3.5、ENTFERNT
Nachdem wir den Löschvorgang überprüft haben, löschen wir die Datei txt2.txt im Ordner „from“ und führen sie aus, um sie anzuzeigen
> Task :app:CopyTask
isIncremental = true
ChangeType = REMOVED , ChangeFile = txt2.txt
Es handelt sich immer noch um einen inkrementellen Build und zeigt an, dass die Datei txt2.txt gelöscht wurde.
Die Verzeichnisstruktur zu diesem Zeitpunkt:
├── app
│ ├── from
│ │ └── txt1.txt
│ └── to
│ ├── txt1.txt
│ └── txt2.txt
Es stellt sich heraus, dass die Datei txt2.txt im Ordner „To“ immer noch vorhanden ist, obwohl wir die Datei „txt2.txt“ im Ordner „From“ gelöscht haben und die Aktion der Aufgabe inkrementelle Builds unterstützt Wenn sich die Änderung auf Ihre Build-Ergebnisse auswirkt, müssen Sie sich trotzdem darum kümmern, um sie synchron zu halten.
9.4. Inkrementelle vs. volle Lautstärke
Nicht jede Ausführung von Task ist ein inkrementeller Build. Wir können die isIncremental-Methode von InputChanges verwenden, um zu bestimmen, ob es sich bei diesem Build um einen inkrementellen Build handelt. Es gibt jedoch die folgenden Situationen, in denen ein vollständiger Build durchgeführt wird:
- Die Aufgabe wird zum ersten Mal ausgeführt;
- Die Aufgabe hat nur Eingaben, aber keine Ausgabe;
- Die upToDateWhen-Bedingung der Aufgabe gibt „false“ zurück;
- Eine der Ausgabedateien dieser Aufgabe hat sich seit dem letzten Build geändert;
- Seit dem letzten Build hat sich eine Attributeingabe der Aufgabe geändert, z. B. einige grundlegende Attributtypen;
- Seit dem letzten Build hat sich eine nicht inkrementelle Dateieingabe der Aufgabe geändert. Eine nicht inkrementelle Dateieingabe bezieht sich auf eine Dateieingabe, die nicht mit @Incremental oder @SkipWhenEmpty annotiert ist.
Wenn sich die Aufgabe im vollständigen Aufbau befindet, das heißt, wenn die isIncremental-Methode von InputChanges false zurückgibt, können alle Eingabedateien über die getFileChanges-Methode von InputChanges abgerufen werden, und der ChangeType jeder Datei wird HINZUGEFÜGT. Wenn sich die Aufgabe im inkrementellen Aufbau befindet, Das heißt, wenn die InputChanges-Methode „true“ zurückgibt, kann die Eingabedatei, die sich nur ändert, über die getFileChanges-Methode von InputChanges abgerufen werden.
9.5. Häufig verwendete Anmerkungstypen
Anmerkung | Typ | Bedeutung |
---|---|---|
@ Eingabe | Jeder serialisierbare Typ oder Ergebnistyp der Abhängigkeitsauflösung | Ein einfacher Eingabewert oder ein Ergebnis der Abhängigkeitsauflösung |
@ InputFile | Datei* | eine einzelne Eingabedatei (kein Verzeichnis) |
@ InputDirectory | Datei* | ein einzelnes Eingabeverzeichnis (keine Datei) |
@ InputFiles | Wiederholbar* | Iterierbar von Eingabedateien und Verzeichnissen |
@ Ausgabedatei | Datei* | eine einzelne Ausgabedatei (kein Verzeichnis) |
@ Ausgabeverzeichnis | Datei* | ein einzelnes Ausgabeverzeichnis (keine Datei) |
@ OutputFiles | Map<String, File>* oder Iterable | Iterierbar oder Karte von Ausgabedateien. Durch die Verwendung eines Dateibaums wird das Caching für Aufgaben deaktiviert . |
@ Ausgabeverzeichnisse | Map<String, File>* oder Iterable | Iterierbare Ausgabeverzeichnisse. Durch die Verwendung eines Dateibaums wird das Caching für Aufgaben deaktiviert . |
@ Verschachtelt | jeder benutzerdefinierte Typ | Ein benutzerdefinierter Typ, der Serializable möglicherweise nicht implementiert, aber über mindestens ein Feld oder eine Eigenschaft verfügt, die mit einer der Anmerkungen in dieser Tabelle gekennzeichnet ist. Es könnte sogar ein anderes @Nested sein. |
@ Intern | jeder Typ | Gibt an, dass die Eigenschaft intern verwendet wird, aber weder eine Eingabe noch eine Ausgabe ist. |
@ SkipWhenEmpty | Datei oder iterierbar* | Wird mit @InputFiles oder @InputDirectory verwendet und weist Gradle an, die Aufgabe zu überspringen, wenn die entsprechende Datei oder das entsprechende Verzeichnis sowie alle anderen mit dieser Annotation deklarierten Eingabedateien leer sind. Aufgaben, die übersprungen werden, weil alle Eingabedateien diese Anmerkung als leer deklarieren, führen scheinbar zu „Keine Quelle“. Beispielsweise wird NO-SOURCE in der Konsolenausgabe ausgegeben. |
Hinweis @Inkremental . | ||
@ Inkrementell | jeder Typ | Wird mit @InputFiles oder @InputDirectory verwendet und weist Gradle an, Änderungen an mit Anmerkungen versehenen Dateieigenschaften zu verfolgen, sodass Änderungen über @ InputChanges.getFileChanges() abgefragt werden können . Inkrementelle Aufgaben erforderlich. |
@ Optional | jeder Typ | Wird mit allen in der optionalen API-Dokumentation aufgeführten Attributtypanmerkungen verwendet. Diese Anmerkung deaktiviert Validierungsprüfungen für die entsprechende Eigenschaft. Weitere Einzelheiten finden Sie im Abschnitt „Validierung“ . |
Weitere Informationen finden Sie im Dokument .
9.6. Inkrementelles Konstruktionsprinzip
Bevor Gradle die Aufgabe zum ersten Mal ausführt, erfasst er den Fingerabdruck der Eingabe, der den Pfad der Eingabedatei und einen Hash des Inhalts jeder Datei enthält. Führen Sie dann die Aufgabe aus. Wenn die Aufgabe erfolgreich abgeschlossen wird, erhält Gradle den Ausgabe-Fingerabdruck, der eine Reihe von Ausgabedateien und einen Hash des Inhalts jeder Datei enthält. Gradle behält bei der nächsten Ausführung der Aufgabe zwei Fingerabdrücke.
Jedes weitere Mal vor der Ausführung der Aufgabe führt Gradle einen neuen Fingerabdruck für die Ein- und Ausgabe durch. Wenn der neue Fingerabdruck mit dem vorherigen Fingerabdruck übereinstimmt, geht Gradle davon aus, dass die Ausgabe die neueste ist, und überspringt die Aufgabe. Wenn sie nicht identisch sind , Gradle führt die Aufgabe aus. . Gradle behält bei der nächsten Ausführung der Aufgabe zwei Fingerabdrücke.
Wenn sich die Statistiken der Datei (d. h. lastModified und Größe) nicht geändert haben, verwendet Gradle den Datei-Fingerabdruck aus der letzten Ausführung wieder, d. h. wenn sich die Statistiken der Datei nicht geändert haben, erkennt Gradle keine Änderungen.
Gradle betrachtet den Code der Aufgabe auch als Teil der Eingabe der Aufgabe. Wenn sich die Aufgabe, die Aktion oder ihre Abhängigkeiten zwischen den Ausführungen ändern, betrachtet Gradle die Aufgabe als veraltet.
Gradle versteht, ob Dateieigenschaften (z. B. diejenigen, die den Java-Klassenpfad enthalten) reihenfolgeempfindlich sind, und wenn die Fingerabdrücke dieser Eigenschaften verglichen werden, führt dies dazu, dass die Aufgabe veraltet ist, selbst wenn sich die Dateireihenfolge ändert.
Beachten Sie, dass, wenn eine Aufgabe ein Ausgabeverzeichnis angibt, alle seit der letzten Ausführung zu diesem Verzeichnis hinzugefügten Dateien ignoriert werden und nicht dazu führen, dass die Aufgabe veraltet ist. Daher können nicht verwandte Aufgaben ein Ausgabeverzeichnis ohne gegenseitige Beeinträchtigung gemeinsam nutzen, wenn dies aus irgendeinem Grund der Fall ist nicht das gewünschte Verhalten ist, sollten Sie die Verwendung von TaskOutputs.upToDateWhen(groovy.lang.Closure) in Betracht ziehen.
Beachten Sie auch, dass Änderungen an der Verfügbarkeit nicht verfügbarer Dateien (z. B. das Ändern des Ziels eines defekten Symlinks in eine gültige Datei und umgekehrt) durch aktuelle Überprüfungen erkannt und behandelt werden.
Die Eingabe der Aufgabe wird auch zur Berechnung des Build-Cache-Schlüssels verwendet, der zum Laden der Ausgabe der Aufgabe verwendet wird, wenn diese aktiviert ist.
10. Aufgabe suchen
Manchmal müssen wir eine offizielle Aufgabe finden, um Vorgänge zu verknüpfen, z. B. das Hinzufügen einer Aktion; manchmal können wir auch eine benutzerdefinierte Aufgabe finden, um sie von einer offiziellen Aufgabe abhängig zu machen.
Beim Finden der Aufgabe handelt es sich hauptsächlich um das TaskContainer-Objekt. Wie der Name schon sagt, handelt es sich um die Verwaltungsklasse des Task-Containers, die zwei Methoden bereitstellt:
- findByPath(String path), der Parameter kann leer sein
- getByPath(String path), der Parameter kann leer sein. Wenn die Aufgabe nicht gefunden werden kann, wird eine UnknownTaskException ausgelöst
Gleichzeitig erbt TaskContainer von zwei Methoden, die verwendet werden können, TaskCollection
und fügt diese hinzu:NamedDomainObjectCollection
- findByName
- getByName
Die Parameterdefinition ist dieselbe wie bei der xxxByPath-Methode.
10.1、findByName
Beispiel:
def aaa = tasks.findByName("yechaoa").doFirst {
println("yechaoa excuted doFirst by findByName")
}
Suchen Sie eine Aufgabe mit dem Namen „yechaoa“, fügen Sie eine doFirst-Aktion hinzu und drucken Sie das Protokoll in doFirst aus.
Wenn wir zu diesem Zeitpunkt aaa ausführen, wird die Ausführung der Yechaoa-Aufgabenaktion nicht ausgelöst, da keine Abhängigkeit besteht und wir daher die Yechaoa-Aufgabe ausführen müssen.
implementieren:
./gradlew yechaoa
Ausgang:
> Task :app:yechaoa
yechaoa excuted doFirst by findByName
...
Sie können sehen, dass das von uns hinzugefügte Protokoll ausgedruckt wurde.
10.2、findByPath
Beispiel:
def bbb = tasks.findByPath("yechaoa").doFirst {
println("yechaoa excuted doFirst by findByPath")
}
Ausgang:
> Task :app:yechaoa
yechaoa excuted doFirst by findByPath
yechaoa excuted doFirst by findByName
...
10.3、benannt
Suchen Sie die Aufgabe anhand des Namens. Wenn keine Ausnahme vorliegt, wird eine UnknownTaskException ausgelöst
tasks.named("yechaoa") {
it.doFirst {
println("yechaoa excuted doFirst by named")
}
}
Ausgang:
> Task :app:yechaoa
yechaoa excuted doFirst by named
yechaoa excuted doFirst by findByPath
yechaoa excuted doFirst by findByName
...
10.4 Sonstiges
mitTyp:
tasks.withType(DefaultTask).configureEach(task -> {
if (task.name.toLowerCase().contains("copytask")) {
println(task.class.name)
}
})
every/forEach/configureEach:
tasks.each {
// do something
}
tasks.forEach(task->{
// do something
})
tasks.configureEach(task -> {
// do something
})
11, extra
11.1 Der Unterschied zwischen Registrieren und Erstellen
Zusätzlich zu den Methoden im obigen Beispiel register
gibt es create
Methoden zum Erstellen einer Aufgabe. Was ist also der Unterschied zwischen ihnen?
- Bei der Erstellung per Register wird diese Aufgabe nur dann erstellt und konfiguriert, wenn sie benötigt wird.
- Bei der Erstellung durch create wird die Aufgabe sofort erstellt und konfiguriert und dem TaskContainer hinzugefügt.
Um es ganz klar auszudrücken: Registrieren ist eine Möglichkeit, Aufgaben bei Bedarf zu erstellen, sodass die Leistung der Gradle-Ausführung besser ist (nicht die Leistung Ihres Projekts).
Die Art und Weise, wie create eine Aufgabe erstellt, wird nicht mehr offiziell empfohlen. Obwohl @Deprecated noch nicht markiert ist, wird es möglicherweise in Zukunft aufgegeben.
Es ist jedoch zu beachten, dass das Register verzögert geladen wird und die durch Verschachtelung erstellte Aufgabe während der Konfigurationsphase nicht initialisiert werden kann, sodass sie nicht ausgeführt wird.
Zusätzlich zum Registrieren und Erstellen gibt es auch eine Ersetzungsmethode, mit der die vorhandene Aufgabe durch den Namen ersetzt wird.
11.2、Aufgabenbaum
Wir können ./gradlew tasks
alle Aufgaben über einsehen, aber wir können die Abhängigkeiten der Aufgaben nicht sehen.
Um die Abhängigkeiten einer Aufgabe anzuzeigen, können wir das Task-Tree- Plugin verwenden
plugins {
id "com.dorongold.task-tree" version "2.1.1"
}
taskTree
Wenn wir es verwenden, müssen wir es nur nach dem Befehl hinzufügen
gradle <task 1>...<task N> taskTree
Beispiel:
./gradlew build taskTree
Ausgang:
:app:build
+--- :app:assemble
| +--- :app:assembleDebug
| | +--- :app:mergeDebugNativeDebugMetadata
| | | \--- :app:preDebugBuild
| | | \--- :app:preBuild
| | \--- :app:packageDebug
| | +--- :app:compileDebugJavaWithJavac
| | | +--- :app:compileDebugAidl
| | | | \--- :app:preDebugBuild *
| | | +--- :app:compileDebugKotlin
......
12. Endlich
Bisher ist die Einführung von Gradle Task abgeschlossen.
Unter den Gesichtspunkten, was eine Aufgabe ist, wo sie geschrieben wird, wie sie geschrieben wird, wie sie ausgeführt wird und wie man sie gut schreibt usw., wird die Ausführungssequenz von Aufgabenaktionen, benutzerdefinierten Aufgaben, Aufgabenabhängigkeiten usw. vorgestellt Inkrementelle Aufgabenkonstruktion von flach nach tief. Im Allgemeinen sind immer noch viele Wissenspunkte erforderlich, die verstanden und in der Praxis angewendet werden müssen.
Ich hoffe, dieser Artikel ist hilfreich für Sie
13、Github
https://github.com/yechaoa/GradleX