[Gradle-7] Gradle Build Core-Aufgabenhandbuch

1. Einleitung

Dieser Artikel ist der siebte Artikel der Gradle-Reihe , der Ihnen Taskdie Wissenspunkte zum Kern der Gradle-Konstruktion vermittelt.

2. Was ist die Aufgabe in Gradle?

Taskist eine Aufgabe und Gradledie kleinste davon 构建单元.
Der Kern der Gradle-Konstruktion ist ein gerichteter azyklischer Graph, der aus Task besteht:
Bild.png
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 ActionListe. 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, Projectwird 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:

  1. register(String name, Action<? super Task> ConfigurationAction)
  2. register(String-Name, Klassentyp, Action<? super T> ConfigurationAction)
  3. register(Stringname, Klassentyp)
  4. register(String-Name, Klassentyp, Objekt… KonstruktorArgs)
  5. 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:

  1. Weder Eingabe noch Ausgabe haben sich geändert;
  2. Die Ausgabe hat sich nicht geändert;
  3. Die Aufgabe hat keine Operation und Abhängigkeiten, aber der Inhalt der Abhängigkeiten ist der neueste oder wird übersprungen oder wiederverwendet.
  4. 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 @TaskActionmit Anmerkungen versehen werden.
  • Für extern verfügbar gemachte Parameter müssen @InternalAnmerkungen 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.
Screenshot 2023-06-18 00.26.56.png

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 StopExecutionExceptionzum Ü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 enabledSchalter: „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 timeoutEigenschaften 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.
Bild.png
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. @InputXXXbedeutet Eingabe, @OutputXXXbedeutet Ausgabe.

  • Der obige Code fromist unsere Eingabe, also die zu kopierende Datei;
  • toist 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.
WX20230619-011700.png
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 executedgeä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 InputChangesder 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 @Incrementaloder 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:

  1. Fügen Sie @IncrementalAnmerkungen zum from-Attribut hinzu, um inkrementelle Eingabeattribute anzuzeigen.
  2. Die Aktionsmethode „execute()“ wurde neu geschrieben und es wurden InputChangesParameter hinzugefügt, um das inkrementelle Kopieren von Dateien zu unterstützen. Dann ChangeTypewerden 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, TaskCollectionund 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 registergibt es createMethoden 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 tasksalle 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"
}

taskTreeWenn 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

14. Verwandte Dokumente

Supongo que te gusta

Origin blog.csdn.net/yechaoa/article/details/131368728
Recomendado
Clasificación