Migliora le prestazioni dell'app con le coroutine Kotlin

Coroutine kotlin consentono di scrivere codice asincrono chiaro e semplificato che la tua app reattiva durante la gestione di attività di lunga durata come le chiamate di rete per le operazioni su disco.

Questo argomento fornisce uno sguardo dettagliato sulle coroutine su Android. Se non conoscete le coroutine, assicuratevi di leggere Kotlin coroutines su Android prima di leggere questo argomento.

Gestisci attività di lunga durata

Le coronetine si basano su funzioni regolari aggiungendo due operazioni per gestire per attività di lunga durata. Oltre a invoke (o call) e return, le coroutine aggiungono suspend e resume:

  • suspend mette in pausa l'esecuzione della coroutine attuale, salvando tutti i contenuti locali come la codifica one-hot delle variabili categoriche.
  • resume continua l'esecuzione di una coroutine sospesa dal luogo in cui è stato sospeso.

Puoi chiamare le funzioni di suspend solo da altre funzioni di suspend oppure utilizzando uno strumento per la creazione di coroutine come launch per iniziare una nuova coroutine.

L'esempio seguente mostra una semplice implementazione della coroutine per una un'attività ipotetica a lunga esecuzione:

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 questo esempio, get() viene ancora eseguito sul thread principale, ma sospende la coroutine prima di avviare la richiesta di rete. Quando la richiesta di rete viene completata, get ripristina la coroutina sospesa invece di utilizzare un callback per inviare una notifica al thread principale.

Kotlin utilizza un frame stack per gestire la funzione in esecuzione con qualsiasi variabile locale. Quando sospendi una coroutine, lo stack attuale il frame viene copiato e salvato per un secondo momento. Quando riprendi, lo stack frame viene copiato da dove era stata salvata, e la funzione riprenderà a essere eseguita. Anche se il codice può sembrare un normale blocco sequenziale richiesta, la coroutine garantisce che la richiesta di rete eviti il blocco nel thread principale.

Usa le coroutine per la massima sicurezza

Le coroutine Kotlin utilizzano i corrieri per determinare quali thread vengono utilizzati l'esecuzione della coroutine. Per eseguire il codice al di fuori del thread principale, puoi dire a Kotlin coroutine per eseguire il lavoro con il supervisore predefinito o IO. Nella Kotlin, tutte le coroutine devono correre in un centralino, anche quando stanno correndo nel thread principale. Le coroutine possono sospendersi e il supervisore sta responsabile di ripristinarli.

Per specificare dove devono correre le coroutine, Kotlin fornisce tre committenti che puoi utilizzare:

  • Dispatchers.Main: utilizza questo supervisore per eseguire una coroutine sul circuito principale Thread Android. Deve essere usato solo per interagire con l'UI e quando si lavora rapidamente. Gli esempi includono la chiamata delle funzioni suspend, l'esecuzione Operazioni del framework UI Android e aggiornamento LiveData oggetti.
  • Dispatchers.IO: questo supervisore è ottimizzato per eseguire operazioni su disco o rete I/O al di fuori del thread principale. Alcuni esempi includono l'utilizzo Componente sala, lettura o scrittura di file ed esecuzione di operazioni di rete.
  • Dispatchers.Default: questo supervisore è ottimizzato per le prestazioni Lavoro ad alta intensità di CPU al di fuori del thread principale. Esempi di casi d'uso includono l'ordinamento di elenco e analisi del file JSON.

Facendo sempre l'esempio precedente, puoi utilizzare i committenti per ridefinire i Funzione get. Nel corpo di get, chiama withContext(Dispatchers.IO) per crea un blocco che viene eseguito sul pool di thread di I/O. Qualsiasi codice che metti al suo interno viene sempre eseguito tramite il committente IO. Poiché withContext è di per sé funzione di sospensione, la funzione get è anche una funzione di sospensione.

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
}

Con le coroutine, puoi inviare thread con un controllo granulare. Poiché withContext() consente di controllare il pool di thread di qualsiasi riga di codice senza introducendo i callback, puoi applicarli a funzioni molto piccole come da un database o eseguire una richiesta di rete. Una buona prassi è utilizzare withContext() per verificare che ogni funzione sia protetta da principale, il che significa che può chiamare la funzione dal thread principale. In questo modo, il chiamante non deve mai pensa a quale thread dovrebbe essere usato per eseguire la funzione.

Nell'esempio precedente, fetchDocs() viene eseguito sul thread principale. ma può chiamare in modo sicuro get, che esegue una richiesta di rete in background. Poiché le coroutine supportano suspend e resume, le coroutine sul il thread viene ripreso con il risultato get non appena il blocco withContext viene fatto.

Rendimento di withContext()

withContext() non aumenta il sovraccarico rispetto a un equivalente basato su callback implementazione. Inoltre, è possibile ottimizzare le chiamate withContext() al di là di un'implementazione equivalente basata su callback in alcune situazioni. Per Ad esempio, se una funzione effettua dieci chiamate a una rete, puoi dire a Kotlin cambiare thread una sola volta usando un withContext() esterno. Poi, anche se la libreria di rete utilizza withContext() più volte, rimane nella stessa il supervisore ed evita di cambiare thread. Inoltre, Kotlin ottimizza il passaggio tra Dispatchers.Default e Dispatchers.IO per evitare i cambi di thread ove possibile.

Inizia una coroutine

Puoi iniziare le coroutine in uno dei due modi seguenti:

  • launch avvia una nuova coroutine e non restituisce il risultato al chiamante. Qualsiasi lavoro considerato "fuoco e dimentica" può essere avviato utilizzando launch.
  • async avvia una nuova coroutine e ti consente di restituire un risultato con una sospensione funzione chiamata await.

Di solito, devi launch una nuova coroutina da una funzione regolare, Come una funzione normale, non può chiamare await. Usa async solo se all'interno un'altra coroutina o l'interno di una funzione di sospensione e l'esecuzione la decomposizione parallela.

Decomposizione parallela

Tutte le coroutine avviate all'interno di una funzione suspend devono essere interrotte quando questa funzione restituisce, quindi è probabile che tu debba garantire che le coroutine terminare prima di tornare. Con la contemporaneità strutturata in Kotlin, puoi definire un coroutineScope che avvia una o più coroutine. Quindi, utilizzando await() (per una singola coroutine) o awaitAll() (per più coroutine), puoi garantire che queste coroutine finiscano prima di tornare dalla funzione.

Ad esempio, definiamo un'istruzione coroutineScope che recupera due documenti in modo asincrono. Chiamando await() su ogni riferimento differito, garantiamo che entrambe le operazioni di async terminino prima di restituire un valore:

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

Puoi utilizzare awaitAll() anche nelle raccolte, come mostrato nell'esempio seguente:

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
    }

Anche se fetchTwoDocs() lancia nuove coroutine con async, la funzione usa awaitAll() per attendere che le coroutine lanciate finiscano prima che ritornano. Tuttavia, tieni presente che anche se non avessimo chiamato awaitAll(), Il generatore di coroutineScope non riprende la coroutine che ha chiamato fetchTwoDocs al termine di tutte le nuove coroutine.

Inoltre, coroutineScope rileva eventuali eccezioni lanciate dalle coroutine e lo instrada al chiamante.

Per ulteriori informazioni sulla decomposizione parallela, vedi Composizione di funzioni di sospensione.

Concetti sulle coroutine

CoroutineScope

Un CoroutineScope tiene traccia di qualsiasi coroutine creata utilizzando launch o async. La il lavoro in corso (ovvero le coroutine in esecuzione) può essere annullato chiamando scope.cancel() in qualsiasi momento. In Android, alcune librerie KTX forniscono il proprio CoroutineScope per determinate classi del ciclo di vita. Ad esempio: ViewModel ha un viewModelScope, e Lifecycle ha lifecycleScope. Tuttavia, a differenza di un supervisore, un CoroutineScope non si occupa delle coroutine.

viewModelScope viene utilizzato anche negli esempi della Threading in background su Android con Coroutines. Tuttavia, se devi creare il tuo CoroutineScope per controllare il ciclo di vita delle coroutine in un determinato livello dell'app, puoi crearne uno come segue:

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

Un ambito annullato non può creare altre coroutine. Pertanto, devi chiama scope.cancel() solo quando la classe che ne controlla il ciclo di vita viene distrutto. Quando utilizzi viewModelScope, il parametro Il corso ViewModel annulla il corso automaticamente l'ambito nel metodo onCleared() di ViewModel.

Job

Un Job è un handle di una coroutine. Ogni coroutine che crei con launch oppure async restituisce un'istanza Job che identifica in modo univoco coroutine e ne gestisce il ciclo di vita. Puoi anche passare Job a un CoroutineScope per gestire ulteriormente il suo ciclo di vita, come mostrato di seguito esempio:

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

Contesto Coroutine

Un CoroutineContext definisce il comportamento di una coroutine utilizzando il seguente insieme di elementi:

Per le nuove coroutine create in un ambito, viene creata una nuova istanza Job assegnati alla nuova coroutine e agli altri elementi CoroutineContext vengono ereditati dall'ambito contenitore. Puoi eseguire l'override passando un nuovo CoroutineContext a launch o async personalizzata. Tieni presente che passare un valore di Job a launch o async non ha alcun effetto. poiché una nuova istanza di Job viene sempre assegnata a una nuova coroutine.

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

Risorse aggiuntive sulle coroutine

Per ulteriori risorse sulle coroutine, consulta i seguenti link: