Anwendungsleistung mit Kotlin-Koroutinen verbessern

Mit Kotlin-Coroutinen können Sie übersichtlichen, vereinfachten asynchronen Code schreiben, der Ihre App reaktionsschnell hält und gleichzeitig Aufgaben mit langer Ausführungszeit wie Netzwerkaufrufe oder Festplattenvorgänge verwaltet.

In diesem Thema werden Coroutinen unter Android ausführlich behandelt. Wenn Sie sich nicht mit Koroutinen auskennen, sollten Sie sich vor dem Lesen dieses Themas mit Kotlin-Koroutinen in Android vertraut machen.

Vorgänge mit langer Ausführungszeit verwalten

Coroutinen basieren auf regulären Funktionen und fügen zwei Vorgänge zum Verarbeiten von zeitaufwendigen Aufgaben hinzu. Zusätzlich zu invoke (oder call) und return werden bei Coroutinen suspend und resume hinzugefügt:

  • suspend pausiert die Ausführung der aktuellen Coroutine und speichert alle lokalen Variablen.
  • resume setzt die Ausführung einer angehaltenen Coroutine an der Stelle fort, an der sie angehalten wurde.

suspend-Funktionen können nur von anderen suspend-Funktionen aufgerufen werden oder Sie können einen Coroutine-Builder wie launch verwenden, um eine neue Coroutine zu starten.

Das folgende Beispiel zeigt eine einfache Coroutine-Implementierung für eine hypothetische Aufgabe mit langer Ausführungszeit:

suspend fun fetchDocs() { // Dispatchers.Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    show(result) // Dispatchers.Main
}

suspend fun get(url: String) = withContext(Dispatchers.IO) {
}

In diesem Beispiel wird get() weiterhin im Hauptthread ausgeführt, aber die Coroutine wird angehalten, bevor die Netzwerkanfrage gestartet wird. Wenn die Netzwerkanfrage abgeschlossen ist, setzt get die angehaltene Coroutine fort, anstatt den Hauptthread über einen Callback zu benachrichtigen.

In Kotlin wird ein Stapelframe verwendet, um zu verwalten, welche Funktion zusammen mit lokalen Variablen ausgeführt wird. Beim Anhalten einer Coroutine wird der aktuelle Stapelrahmen kopiert und für später gespeichert. Beim Fortsetzen wird der Stapelrahmen von der Stelle, an der er gespeichert wurde, zurückkopiert und die Funktion wird wieder ausgeführt. Auch wenn der Code wie eine normale sequenzielle blockierende Anfrage aussieht, sorgt die Coroutine dafür, dass die Netzwerkanfrage den Hauptthread nicht blockiert.

Coroutinen für Main-Safety verwenden

Kotlin-Coroutinen verwenden Dispatcher, um zu bestimmen, welche Threads für die Ausführung von Coroutinen verwendet werden. Wenn Sie Code außerhalb des Hauptthreads ausführen möchten, können Sie Kotlin-Coroutinen anweisen, Aufgaben entweder im Default- oder im IO-Dispatcher auszuführen. In Kotlin müssen alle Coroutinen in einem Dispatcher ausgeführt werden, auch wenn sie im Hauptthread ausgeführt werden. Coroutinen können sich selbst unterbrechen und der Dispatcher ist für das Fortsetzen verantwortlich.

Um anzugeben, wo die Koroutinen ausgeführt werden sollen, bietet Kotlin drei Dispatcher, die Sie verwenden können:

  • Dispatchers.Main: Mit diesem Dispatcher können Sie eine Coroutine im Android-Hauptthread ausführen. Diese Option sollte nur für die Interaktion mit der Benutzeroberfläche und für schnelle Aufgaben verwendet werden. Beispiele hierfür sind das Aufrufen von suspend-Funktionen, das Ausführen von Android-UI-Framework-Vorgängen und das Aktualisieren von LiveData-Objekten.
  • Dispatchers.IO: Dieser Dispatcher ist für die Ausführung von Festplatten- oder Netzwerk-E/A außerhalb des Hauptthreads optimiert. Beispiele hierfür sind die Verwendung der Room-Komponente, das Lesen aus oder Schreiben in Dateien sowie die Ausführung von Netzwerkoperationen.
  • Dispatchers.Default: Dieser Dispatcher ist für die Ausführung von CPU-intensiven Aufgaben außerhalb des Hauptthreads optimiert. Beispiele für Anwendungsfälle sind das Sortieren einer Liste und das Parsen von JSON.

Im vorherigen Beispiel können Sie die Dispatcher verwenden, um die Funktion get neu zu definieren. Rufen Sie im Hauptteil von get die Funktion withContext(Dispatchers.IO) auf, um einen Block zu erstellen, der im IO-Threadpool ausgeführt wird. Jeder Code, den Sie in diesen Block einfügen, wird immer über den IO-Dispatcher ausgeführt. Da withContext selbst eine Suspend-Funktion ist, ist auch die Funktion get eine Suspend-Funktion.

suspend fun fetchDocs() { // Dispatchers.Main
    val result = get("developer.android.com") // Dispatchers.Main
    show(result) // Dispatchers.Main
}

suspend fun get(url: String) = // Dispatchers.Main
    withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
        /* perform network IO here */ // Dispatchers.IO (main-safety block)
    } // Dispatchers.Main

Mit Coroutinen können Sie Threads mit detaillierter Steuerung verteilen. Da Sie mit withContext() den Threadpool jeder Codezeile steuern können, ohne Callbacks einzuführen, können Sie ihn auf sehr kleine Funktionen anwenden, z. B. zum Lesen aus einer Datenbank oder zum Ausführen einer Netzwerkanfrage. Es empfiehlt sich, withContext() zu verwenden, um sicherzustellen, dass jede Funktion main-safe ist. Das bedeutet, dass Sie die Funktion vom Hauptthread aus aufrufen können. So muss sich der Aufrufer nie Gedanken darüber machen, welcher Thread für die Ausführung der Funktion verwendet werden soll.

Im vorherigen Beispiel wird fetchDocs() im Hauptthread ausgeführt. Es kann jedoch sicher get aufrufen, wodurch eine Netzwerkanfrage im Hintergrund ausgeführt wird. Da Coroutinen suspend und resume unterstützen, wird die Coroutine im Hauptthread mit dem Ergebnis get fortgesetzt, sobald der withContext-Block abgeschlossen ist.

Leistung von withContext()

withContext() verursacht keinen zusätzlichen Aufwand im Vergleich zu einer entsprechenden Callback-basierten Implementierung. Außerdem ist es in einigen Fällen möglich, withContext()-Aufrufe über eine entsprechende Callback-basierte Implementierung hinaus zu optimieren. Wenn eine Funktion beispielsweise zehn Aufrufe an ein Netzwerk sendet, können Sie Kotlin mit einem äußeren withContext() anweisen, den Thread nur einmal zu wechseln. Auch wenn die Netzwerkbibliothek withContext() mehrmals verwendet, bleibt sie auf demselben Dispatcher und vermeidet das Wechseln von Threads. Außerdem optimiert Kotlin den Wechsel zwischen Dispatchers.Default und Dispatchers.IO, um Thread-Wechsel nach Möglichkeit zu vermeiden.

Coroutine starten

Sie haben zwei Möglichkeiten, Coroutinen zu starten:

  • launch startet eine neue Coroutine und gibt das Ergebnis nicht an den Aufrufer zurück. Alle Aufgaben, die als „fire and forget“ gelten, können mit launch gestartet werden.
  • async startet eine neue Coroutine und ermöglicht es Ihnen, ein Ergebnis mit einer Suspend-Funktion namens await zurückzugeben.

In der Regel sollten Sie eine neue Coroutine über eine reguläre Funktion launch, da eine reguläre Funktion await nicht aufrufen kann. Verwenden Sie async nur innerhalb einer anderen Coroutine oder innerhalb einer suspend-Funktion, wenn Sie eine parallele Zerlegung durchführen.

Parallele Aufteilung

Alle Coroutinen, die in einer suspend-Funktion gestartet werden, müssen beendet werden, wenn diese Funktion zurückgegeben wird. Sie müssen also wahrscheinlich dafür sorgen, dass diese Coroutinen vor der Rückgabe abgeschlossen werden. Mit strukturierter Nebenläufigkeit in Kotlin können Sie einen coroutineScope definieren, der eine oder mehrere Coroutinen startet. Mit await() (für eine einzelne Coroutine) oder awaitAll() (für mehrere Coroutinen) können Sie dann dafür sorgen, dass diese Coroutinen abgeschlossen werden, bevor die Funktion zurückgegeben wird.

Sehen wir uns als Beispiel eine coroutineScope an, mit der zwei Dokumente asynchron abgerufen werden. Durch den Aufruf von await() für jede verzögerte Referenz wird sichergestellt, dass beide async-Vorgänge abgeschlossen sind, bevor ein Wert zurückgegeben wird:

suspend fun fetchTwoDocs() =
    coroutineScope {
        val deferredOne = async { fetchDoc(1) }
        val deferredTwo = async { fetchDoc(2) }
        deferredOne.await()
        deferredTwo.await()
    }

Sie können awaitAll() auch für Sammlungen verwenden, wie im folgenden Beispiel gezeigt:

suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
    coroutineScope {
        val deferreds = listOf( // fetch two docs at the same time
            async { fetchDoc(1) }, // async returns a result for the first doc
            async { fetchDoc(2) } // async returns a result for the second doc
        )
        deferreds.awaitAll() // use awaitAll to wait for both network requests
    }

Obwohl fetchTwoDocs() neue Koroutinen mit async startet, wartet die Funktion mit awaitAll() darauf, dass diese gestarteten Koroutinen abgeschlossen werden, bevor sie zurückgegeben wird. Auch wenn wir awaitAll() nicht aufgerufen hätten, würde der coroutineScope-Builder die Coroutine, die fetchTwoDocs aufgerufen hat, erst fortsetzen, nachdem alle neuen Coroutinen abgeschlossen sind.

Außerdem fängt coroutineScope alle Ausnahmen ab, die von den Coroutinen ausgelöst werden, und leitet sie an den Aufrufer zurück.

Weitere Informationen zur parallelen Zerlegung finden Sie unter Suspend-Funktionen zusammensetzen.

Konzepte für Coroutinen

CoroutineScope

Ein CoroutineScope verfolgt alle mit launch oder async erstellten Coroutinen. Die laufende Arbeit (d.h. die ausgeführten Coroutinen) kann jederzeit durch Aufrufen von scope.cancel() abgebrochen werden. In Android stellen einige KTX-Bibliotheken eigene CoroutineScope für bestimmte Lifecycle-Klassen bereit. ViewModel hat beispielsweise eine viewModelScope und Lifecycle hat lifecycleScope. Im Gegensatz zu einem Dispatcher führt ein CoroutineScope die Coroutinen jedoch nicht aus.

viewModelScope wird auch in den Beispielen unter Background threading on Android with Coroutines (Hintergrund-Threading unter Android mit Koroutinen) verwendet. Wenn Sie jedoch ein eigenes CoroutineScope erstellen müssen, um den Lebenszyklus von Coroutinen in einer bestimmten Ebene Ihrer App zu steuern, können Sie es so erstellen:

class ExampleClass {

    // Job and Dispatcher are combined into a CoroutineContext which
    // will be discussed shortly
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine within the scope
        scope.launch {
            // New coroutine that can call suspend functions
            fetchDocs()
        }
    }

    fun cleanUp() {
        // Cancel the scope to cancel ongoing coroutines work
        scope.cancel()
    }
}

In einem abgebrochenen Bereich können keine weiteren Coroutinen erstellt werden. Rufen Sie scope.cancel() daher nur auf, wenn die Klasse, die den Lebenszyklus steuert, zerstört wird. Wenn Sie viewModelScope verwenden, wird der Bereich durch die Klasse ViewModel automatisch in der Methode onCleared() des ViewModel abgebrochen.

Job

Ein Job ist ein Handle für eine Koroutine. Jede mit launch oder async erstellte Coroutine gibt eine Job-Instanz zurück, die die Coroutine eindeutig identifiziert und ihren Lebenszyklus verwaltet. Sie können auch ein Job an ein CoroutineScope übergeben, um den Lebenszyklus weiter zu verwalten, wie im folgenden Beispiel gezeigt:

class ExampleClass {
    // ...
    fun exampleMethod() {
        // Handle to the coroutine, you can control its lifecycle
        val job = scope.launch {
            // New coroutine
        }

        if (condition) {
            // Cancel the coroutine started above, this doesn't affect the scope
            // this coroutine was launched in
            job.cancel()
        }
    }
}

CoroutineContext

Ein CoroutineContext definiert das Verhalten einer Coroutine mithilfe der folgenden Elemente:

Für neue Coroutinen, die in einem Bereich erstellt werden, wird der neuen Coroutine eine neue Job-Instanz zugewiesen und die anderen CoroutineContext-Elemente werden vom enthaltenden Bereich übernommen. Sie können die geerbten Elemente überschreiben, indem Sie eine neue CoroutineContext an die Funktion launch oder async übergeben. Das Übergeben eines Job an launch oder async hat keine Auswirkungen, da einer neuen Coroutine immer eine neue Instanz von Job zugewiesen wird.

class ExampleClass {
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine on Dispatchers.Main as it's the scope's default
        val job1 = scope.launch {
            // New coroutine with CoroutineName = "coroutine" (default)
        }

        // Starts a new coroutine on Dispatchers.Default
        val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
            // New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
        }
    }
}

Zusätzliche Ressourcen zu Coroutinen

Weitere Informationen zu Coroutines finden Sie unter den folgenden Links: