Anwendungsleistung mit Kotlin-Koroutinen verbessern

Kotlin-Koroutinen ermöglichen es Ihnen, sauberen, vereinfachten asynchronen Code zu schreiben, Deine App reagiert schnell und verwaltet lang andauernde Aufgaben wie Netzwerkaufrufe oder Laufwerkvorgänge.

In diesem Thema erhalten Sie eine detaillierte Beschreibung von gemeinsamen Routinen unter Android. Wenn Sie nicht mit Koroutinen vertraut sind, lesen Sie Kotlin-Coroutinen für Android lesen, bevor Sie dieses Thema lesen.

Lang andauernde Aufgaben verwalten

Koroutinen basieren auf regulären Funktionen und fügen zwei Operationen hinzu, lang andauernden Aufgaben. Zusätzlich zu invoke (oder call) und return Koroutinen fügen suspend und resume hinzu:

  • suspend pausiert die Ausführung der aktuellen Koroutine und speichert alle lokalen Variablen.
  • resume führt eine ausgesetzte Koroutine von diesem Ort aus fort wo es gesperrt wurde.

Sie können suspend-Funktionen nur von anderen suspend-Funktionen aus aufrufen oder indem Sie einen Koroutinen-Builder wie launch verwenden, um eine neue Koroutine zu starten.

Das folgende Beispiel zeigt eine einfache Koroutinenimplementierung 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, coroutine hinzu, bevor die Netzwerkanfrage gestartet wird. Wenn die Netzwerkanfrage abgeschlossen ist, setzt get die angehaltene Koroutine fort, statt einen Callback zu verwenden. um den Hauptthread zu benachrichtigen.

In Kotlin wird ein Stapelframe verwendet, um zu verwalten, welche Funktion ausgeführt wird. mit beliebigen lokalen Variablen. Beim Aussetzen einer Koroutine wird der aktuelle Stapel wird kopiert und für später gespeichert. Beim Fortsetzen wird der Stapelframe wurden von der Stelle, an der sie gespeichert wurden, zurückgesetzt und die Funktion wird wieder ausgeführt. Auch wenn der Code wie eine gewöhnliche sequenzielle Blockierung -Anfrage stellt die Koroutine sicher, dass die Netzwerkanfrage im Hauptthread.

Koroutinen für die Hauptsicherheit verwenden

Kotlin-Koroutinen verwenden Disponenten, um zu bestimmen, welche Threads für Koroutinen. Wenn Sie Code außerhalb des Hauptthreads ausführen möchten, können Sie Kotlin mitteilen, Koroutinen, um Arbeiten auf dem Default- oder IO-Dispatcher auszuführen. In Für Kotlin müssen alle Koroutinen in einem Dispatcher ausgeführt werden, auch wenn sie auf im Hauptthread. Koroutinen können sich selbst aussetzen, während der Disponent für die Wiederaufnahme verantwortlich.

In Kotlin stehen drei Dispatcher zur Verfügung, um anzugeben, wo die Koroutinen ausgeführt werden sollen. die Sie verwenden können:

  • Dispatchers.Main – Verwenden Sie diesen Dispatcher, um eine gemeinsame Routine auf dem Main auszuführen. Android-Thread. Dieser sollte nur für die Interaktion mit der Benutzeroberfläche und schnell Arbeit auszuführen. Beispiele hierfür sind das Aufrufen von suspend-Funktionen, das Ausführen Vorgänge im Android-UI-Framework und Aktualisieren von LiveData-Objekte.
  • Dispatchers.IO: Dieser Dispatcher ist für die Ausführung von Laufwerken oder Netzwerken optimiert. E/A außerhalb des Hauptthreads. Beispiele hierfür sind die Verwendung des Komponente „Raum“ Lesen oder Schreiben in Dateien und Ausführen von Netzwerkvorgängen.
  • Dispatchers.Default – Dieser Dispatcher ist für seine Leistung optimiert. CPU-intensive Arbeit außerhalb des Hauptthreads Beispielanwendungsfälle beinhalten das Sortieren einer und Parsen von JSON.

Ausgehend vom vorherigen Beispiel können Sie die Disponenten dazu verwenden, get. Rufen Sie im Text von get withContext(Dispatchers.IO) auf, um einen Block zu erstellen, der im E/A-Thread-Pool ausgeführt wird. Code, den Sie darin -Block wird immer über den IO-Dispatcher ausgeführt. Da withContext selbst ein Anhalten-Funktion ist die Funktion get ebenfalls eine Anhalten-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 Koroutinen können Sie Threads detailliert steuern. Weil Mit withContext() können Sie den Threadpool jeder Codezeile steuern, ohne Callbacks eingeführt, können Sie diese auf sehr kleine Funktionen anwenden, oder durch eine Netzwerkanfrage. Es hat sich bewährt, withContext(), um dafür zu sorgen, dass jede Funktion main-sicher ist. Das bedeutet, dass Sie die Funktion aus dem Hauptthread aufrufen kann. So muss der Anrufer überlegen Sie sich, welcher Thread zum Ausführen der Funktion verwendet werden soll.

Im vorherigen Beispiel wird fetchDocs() im Hauptthread ausgeführt. Allerdings kann get sicher aufrufen, wodurch im Hintergrund eine Netzwerkanfrage ausgeführt wird. Da Koroutinen suspend und resume unterstützen, wird die Koroutine auf der Hauptseite verwendet. Der Thread wird mit dem Ergebnis get fortgesetzt, sobald der withContext-Block fertig.

Leistung von withContext()

withContext() keinen zusätzlichen Aufwand im Vergleich zu einem gleichwertigen Callback-basierten System mit sich bringt. Implementierung. Darüber hinaus ist es möglich, withContext()-Aufrufe zu optimieren. über eine äquivalente Callback-basierte Implementierung hinaus. Für Beispiel: Wenn eine Funktion zehn Aufrufe an ein Netzwerk sendet, können Sie Kotlin mitteilen, tauschen Threads nur einmal mit einem äußeren withContext(). Dann, obwohl verwendet die Netzwerkbibliothek withContext() mehrmals, bleibt auf derselben und vermeidet den Wechsel von Threads. Außerdem optimiert Kotlin den Wechsel zwischen Dispatchers.Default und Dispatchers.IO, um Thread-Wechsel zu vermeiden wenn möglich.

Koroutine starten

Sie haben zwei Möglichkeiten, Koroutinen zu starten:

  • launch startet eine neue Koroutine und gibt das Ergebnis nicht an den Aufrufer zurück. Beliebig die als „Feuer und Vergessen“ gilt kann mit launch gestartet werden.
  • async startet eine neue Koroutine und ermöglicht Ihnen, ein Ergebnis mit einer Sperrung zurückzugeben mit dem Namen await.

Normalerweise sollten Sie mit launch eine neue Koroutine aus einer regulären Funktion als reguläre Funktion await nicht aufrufen. async nur im Innenbereich verwenden einer anderen Koroutine oder einer Anhalten-Funktion parallele Zerlegung.

Parallelzerlegung

Alle Koroutinen, die in einer suspend-Funktion gestartet werden, müssen beendet werden, wenn diese Funktion zurückgibt. Sie müssen also wahrscheinlich sicherstellen, dass diese Koroutinen bevor Sie zurückkehren. Mit strukturierten Nebenläufigkeit in Kotlin können Sie Eine coroutineScope, die eine oder mehrere Koroutinen startet. Dann mit await() (für eine einzelne Koroutine) oder awaitAll() (für mehrere Koroutinen) können Sie dass diese Koroutinen vor der Rückkehr von der Funktion beendet werden.

Definieren wir als Beispiel ein coroutineScope, das zwei Dokumente abruft asynchron programmiert. Durch den Aufruf von await() für jede zurückgestellte Referenz garantieren wir, 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, führt die Funktion verwendet awaitAll(), um auf den Abschluss dieser gestarteten Koroutinen zu warten, bevor zurückkehrt. Hinweis: Auch wenn awaitAll() nicht aufgerufen wurde, coroutineScope-Builder setzt die Koroutine nicht fort, die aufgerufen wurde fetchTwoDocs, bis alle neuen Koroutinen abgeschlossen sind.

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

Weitere Informationen zur parallelen Zerlegung findest du unter Aussetzende Funktionen erstellen

Konzepte von Koroutinen

KoroutineScope

CoroutineScope verfolgt alle Koroutinen, die es mit launch oder async erzeugt. Die laufende Arbeiten (d.h. die laufenden Koroutinen) können durch Aufrufen von scope.cancel(). In Android bieten einige KTX-Bibliotheken eigene CoroutineScope für bestimmte Lebenszyklusklassen. Beispiel: ViewModel hat einen viewModelScope, und Lifecycle hat lifecycleScope. Im Gegensatz zu einem Dispatcher führt ein CoroutineScope die Koroutinen jedoch nicht aus.

viewModelScope wird auch in den Beispielen verwendet in Threads im Hintergrund unter Android mit Coroutines Wenn Sie jedoch Ihren eigenen CoroutineScope erstellen müssen, um den Lebenszyklus von Koroutinen in einer bestimmten Ebene Ihrer App wie folgt:

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()
    }
}

Ein abgebrochener Bereich kann keine weiteren Koroutinen erstellen. Daher sollten Sie scope.cancel() nur dann aufrufen, wenn die Klasse, die ihren Lebenszyklus steuert, zerstört wird. Bei Verwendung von viewModelScope gibt der Parameter ViewModel-Klasse bricht den automatisch mit der Methode onCleared() von ViewModel.

Job

Job ist ein Handle zu einer Koroutine. Jede Koroutine, die Sie mit launch erstellen, oder async gibt eine Job-Instanz zurück, die das Ereignis und verwaltet deren Lebenszyklus. Sie können auch einen Job an einen CoroutineScope, um den Lebenszyklus weiter zu verwalten, wie unten gezeigt. Beispiel:

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

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

CoroutineContext

CoroutineContext definiert das Verhalten einer Koroutine mithilfe der folgenden Elemente:

Für neue Koroutinen, die innerhalb eines Bereichs erstellt wurden, wird eine neue Job-Instanz erstellt. der neuen Koroutine zugewiesen ist, und die anderen CoroutineContext-Elemente werden aus dem einschließenden Bereich übernommen. Sie können die übernommenen -Elementen durch Übergeben eines neuen CoroutineContext an launch oder async . Die Übergabe von Job an launch oder async hat keine Auswirkungen. als neue Instanz von Job wird immer einer neuen Koroutine zugewiesen.

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 für Koroutinen

Weitere Ressourcen zu Koroutinen finden Sie unter den folgenden Links: