Sean McQuillan
Sean McQuillan

Follow

Apr 30, 2019 – 9 min read

Dit is onderdeel van een meerdelige serie over het gebruik van Coroutines op Android. Deze post richt zich op hoe coroutines werken en welke problemen ze oplossen.

Andere artikelen in deze serie:

Kotlin coroutines introduceren een nieuwe stijl van concurrency die kan worden gebruikt op Android om async code te vereenvoudigen. Hoewel ze nieuw zijn voor Kotlin in 1.3, is het concept van coroutines al bekend sinds de dageraad van programmeertalen. De eerste taal die het gebruik van coroutines onderzocht was Simula in 1967.

In de afgelopen jaren zijn coroutines gegroeid in populariteit en zijn nu opgenomen in veel populaire programmeertalen zoals Javascript, C #, Python, Ruby, en Go om er een paar te noemen. Kotlin coroutines zijn gebaseerd op gevestigde concepten die zijn gebruikt om grote applicaties te bouwen.

Op Android zijn coroutines een geweldige oplossing voor twee problemen:

  1. Langlopende taken zijn taken die te lang duren om de hoofddraad te blokkeren.
  2. Main-safety stelt u in staat om ervoor te zorgen dat elke suspend-functie kan worden aangeroepen vanuit de hoofddraad.

Laten we in elk duiken om te zien hoe coroutines ons kunnen helpen code op een schonere manier te structureren!

Langlopende taken

Het ophalen van een webpagina of interactie met een API impliceren beide het doen van een netwerkverzoek. Evenzo, lezen uit een database of het laden van een afbeelding van schijf impliceert het lezen van een bestand. Dit soort dingen zijn wat ik langlopende taken noem – taken die veel te lang duren om uw app te laten stoppen en erop te wachten!

Het kan moeilijk zijn om te begrijpen hoe snel een moderne telefoon code uitvoert in vergelijking met een netwerkverzoek. Op een Pixel 2 duurt een enkele CPU-cyclus iets minder dan 0,0000000004 seconden, een getal dat vrij moeilijk te vatten is in menselijke termen. Als je echter denkt aan een netwerkverzoek als een oogwenk, ongeveer 400 milliseconden (0,4 seconden), is het gemakkelijker te begrijpen hoe snel de CPU werkt. In één oogwenk, of een ietwat traag netwerkverzoek, kan de CPU meer dan een miljard cycli uitvoeren!

Op Android heeft elke app een hoofddraad die verantwoordelijk is voor het afhandelen van UI (zoals het tekenen van weergaven) en het coördineren van gebruikersinteracties. Als er te veel werk op deze thread gebeurt, lijkt de app te hangen of te vertragen, wat leidt tot een ongewenste gebruikerservaring. Elke langlopende taak moet worden uitgevoerd zonder de hoofddraad te blokkeren, zodat uw app geen zogenaamde “jank” vertoont, zoals bevroren animaties, of traag reageert op aanraakgebeurtenissen.

Om een netwerkverzoek buiten de hoofddraad om uit te voeren, is een veel voorkomend patroon callbacks. Callbacks bieden een handvat aan een bibliotheek die het kan gebruiken om terug te bellen in uw code op een later tijdstip. Met callbacks zou het ophalen van developer.android.com er als volgt uit kunnen zien:

class ViewModel: ViewModel() {
fun fetchDocs() {
get("developer.android.com") { result ->
show(result)
}
}
}

Ondanks dat get wordt aangeroepen vanuit de hoofddraad, zal het een andere draad gebruiken om het netwerkverzoek uit te voeren. Zodra het resultaat van het netwerk beschikbaar is, wordt de callback op de hoofddraad aangeroepen. Dit is een geweldige manier om langlopende taken af te handelen, en bibliotheken zoals Retrofit kunnen u helpen bij het doen van netwerkverzoeken zonder de hoofddraad te blokkeren.

Coroutines gebruiken voor langlopende taken

Coroutines zijn een manier om de code te vereenvoudigen die wordt gebruikt om langlopende taken zoals fetchDocs te beheren. Om te onderzoeken hoe coroutines de code voor langlopende taken eenvoudiger maken, laten we het bovenstaande callback-voorbeeld herschrijven om coroutines te gebruiken.

// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}// look at this in the next section
suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}

Blokkeert deze code de hoofddraad niet? Hoe retourneert het een resultaat van get zonder te wachten op het netwerk verzoek en te blokkeren? Het blijkt dat coroutines een manier bieden voor Kotlin om deze code uit te voeren en nooit de hoofddraad te blokkeren.

Coroutines bouwen voort op reguliere functies door twee nieuwe operaties toe te voegen. Naast aanroepen (of aanroepen) en terugkeren, voegen coroutines opschorten en hervatten toe.

  • opschorten – pauzeert de uitvoering van de huidige coroutine, waarbij alle lokale variabelen worden opgeslagen
  • hervatten – zet een opgeschorte coroutine voort vanaf de plaats waar deze werd gepauzeerd

Deze functionaliteit wordt door Kotlin toegevoegd door het suspend keyword op de functie. U kunt suspend-functies alleen aanroepen vanuit andere suspend-functies, of door een coroutinebouwer als launch te gebruiken om een nieuwe coroutine te starten.

Suspend en resume werken samen om callbacks te vervangen.

In het bovenstaande voorbeeld zal get de coroutine opschorten voordat deze het netwerkverzoek start. De functie get is nog steeds verantwoordelijk voor het uitvoeren van het netwerkverzoek vanuit de hoofddraad. Wanneer het netwerkverzoek is voltooid, hoeft de hoofddraad niet te worden geïnformeerd via een callback, maar kan de coroutine die is onderbroken, gewoon worden hervat.

Animatie die laat zien hoe Kotlin suspend en resume implementeert om callbacks te vervangen.

Als je kijkt naar hoe fetchDocs wordt uitgevoerd, kun je zien hoe suspend werkt. Wanneer een coroutine wordt opgeschort, wordt het huidige stackframe (de plaats die Kotlin gebruikt om bij te houden welke functie wordt uitgevoerd en de variabelen) gekopieerd en opgeslagen voor later. Wanneer het hervat wordt, wordt het stack frame terug gekopieerd van waar het was opgeslagen en begint het weer te lopen. In het midden van de animatie – wanneer alle coroutines op de hoofddraad zijn opgeschort, is de hoofddraad vrij om het scherm bij te werken en gebruikersgebeurtenissen af te handelen. Samen vervangen suspend en resume callbacks. Best handig!

Wanneer alle coroutines op de hoofddraad zijn opgeschort, is de hoofddraad vrij om ander werk te doen.

Ondanks dat we eenvoudige sequentiële code hebben geschreven die precies lijkt op een blokkerend netwerkverzoek, voeren coroutines onze code precies uit zoals wij dat willen en wordt voorkomen dat de hoofddraad wordt geblokkeerd!

Volgende, laten we eens kijken hoe we coroutines kunnen gebruiken voor main-safety en hoe we dispatchers kunnen verkennen.

Main-safety met coroutines

In Kotlin coroutines, zijn goed geschreven suspend functies altijd veilig om aan te roepen vanuit de main thread. Wat ze ook doen, ze moeten altijd toestaan dat elke thread ze aanroept.

Maar, er zijn veel dingen die we in onze Android-apps doen die te traag zijn om op de hoofddraad te gebeuren. Netwerk verzoeken, parsing JSON, lezen of schrijven uit de database, of zelfs gewoon itereren over grote lijsten. Elk van deze heeft het potentieel om langzaam genoeg te lopen om door de gebruiker zichtbare “jank” te veroorzaken en zou van de hoofddraad moeten lopen.

Het gebruik van suspend vertelt Kotlin niet om een functie op een achtergronddraad uit te voeren. Het is de moeite waard om duidelijk en vaak te zeggen dat coroutines op de hoofddraad worden uitgevoerd. Het is zelfs een heel goed idee om Dispatchers.Main.immediate te gebruiken als je een coroutine start in reactie op een UI-event – op die manier kan, als je uiteindelijk geen langlopende taak uitvoert die main-safety vereist, het resultaat beschikbaar zijn in het eerstvolgende frame voor de gebruiker.

Coroutines worden uitgevoerd op de hoofddraad, en suspend betekent niet achtergrond.

Om een functie die werk doet dat te traag is voor de hoofddraad main-safe te maken, kunt u Kotlin-coroutines vertellen om werk uit te voeren op ofwel de Default– of IO-dispatcher. In Kotlin, moeten alle coroutines in een dispatcher draaien – zelfs als ze op de main thread draaien. Coroutines kunnen zichzelf opschorten, en de dispatcher is het ding dat weet hoe ze te hervatten.

Om te specificeren waar de coroutines moeten draaien, biedt Kotlin drie Dispatchers die je kunt gebruiken voor thread dispatch.

+-----------------------------------+
| Dispatchers.Main |
+-----------------------------------+
| Main thread on Android, interact |
| with the UI and perform light |
| work |
+-----------------------------------+
| - Calling suspend functions |
| - Call UI functions |
| - Updating LiveData |
+-----------------------------------+
+-----------------------------------+
| Dispatchers.IO |
+-----------------------------------+
| Optimized for disk and network IO |
| off the main thread |
+-----------------------------------+
| - Database* |
| - Reading/writing files |
| - Networking** |
+-----------------------------------+
+-----------------------------------+
| Dispatchers.Default |
+-----------------------------------+
| Optimized for CPU intensive work |
| off the main thread |
+-----------------------------------+
| - Sorting a list |
| - Parsing JSON |
| - DiffUtils |
+-----------------------------------+

* Room zorgt automatisch voor main-safety als je suspend functies, RxJava, of LiveData gebruikt.

** Netwerkbibliotheken zoals Retrofit en Volley beheren hun eigen threads en vereisen geen expliciete main-safety in uw code bij gebruik met Kotlin coroutines.

Om verder te gaan met het bovenstaande voorbeeld, laten we de dispatchers gebruiken om de get functie te definiëren. In de body van get roep je withContext(Dispatchers.IO) aan om een blok te maken dat zal draaien op de IO dispatcher. Elke code die u in dat blok zet zal altijd worden uitgevoerd op de IO dispatcher. Omdat withContext zelf een suspend functie is, zal het werken met coroutines om hoofdveiligheid te bieden.

// Dispatchers.Main
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
/* perform blocking network IO here */
}
// Dispatchers.Main

Met coroutines kun je thread dispatch doen met fijnkorrelige controle. Omdat withContext u laat bepalen op welke thread een regel code wordt uitgevoerd zonder een callback te introduceren om het resultaat terug te geven, kunt u het toepassen op zeer kleine functies zoals het lezen uit uw database of het uitvoeren van een netwerk verzoek. Een goed gebruik is dus om withContext te gebruiken om er zeker van te zijn dat elke functie veilig kan worden aangeroepen op elke Dispatcher inclusief Main – op die manier hoeft de aanroeper nooit na te denken over welke thread nodig zal zijn om de functie uit te voeren.

In dit voorbeeld wordt fetchDocs uitgevoerd op de hoofddraad, maar kan get veilig aanroepen die een netwerkverzoek op de achtergrond uitvoert. Omdat coroutines suspend en resume ondersteunen, wordt de coroutine op de hoofddraad hervat met het resultaat zodra het withContext-blok is voltooid.

Goed geschreven suspend-functies kunnen altijd veilig worden aangeroepen vanuit de hoofddraad (of main-safe).

Het is een heel goed idee om elke suspend-functie main-safe te maken. Als de functie iets doet dat de schijf of het netwerk raakt, of zelfs gewoon te veel CPU gebruikt, gebruik dan withContext om hem veilig te maken voor aanroepen vanuit de hoofddraad. Dit is het patroon dat op coroutines gebaseerde bibliotheken zoals Retrofit en Room volgen. Als je deze stijl in je hele codebase volgt, wordt je code veel eenvoudiger en voorkom je dat threading problemen zich vermengen met applicatielogica. Wanneer consequent gevolgd, zijn coroutines vrij om op de hoofddraad te starten en netwerk- of databaseverzoeken te doen met eenvoudige code, terwijl gebruikers gegarandeerd geen “jank” te zien krijgen.”

Prestaties van withContext

withContext is net zo snel als callbacks of RxJava voor het bieden van hoofdveiligheid. Het is zelfs mogelijk om withContext aanroepen te optimaliseren voorbij wat mogelijk is met callbacks in sommige situaties. Als een functie 10 aanroepen zal doen naar een database, kun je Kotlin vertellen om eenmaal in een buitenste withContext te schakelen rond alle 10 aanroepen. Dan, ook al zal de database bibliotheek withContext herhaaldelijk aanroepen, zal het op dezelfde dispatcher blijven en een snel-pad volgen. Bovendien is het schakelen tussen Dispatchers.Default en Dispatchers.IO geoptimaliseerd om thread-switches waar mogelijk te vermijden.

What’s next

In dit bericht hebben we onderzocht welke problemen coroutines goed kunnen oplossen. Coroutines zijn een echt oud concept in programmeertalen die de laatste tijd populair zijn geworden vanwege hun vermogen om code die interageert met het netwerk eenvoudiger te maken.

Op Android kun je ze gebruiken om twee echt veel voorkomende problemen op te lossen:

  1. Vereenvoudiging van de code voor langlopende taken zoals het lezen van het netwerk, schijf, of zelfs het parseren van een groot JSON-resultaat.
  2. Het uitvoeren van precieze main-safety om ervoor te zorgen dat je nooit per ongeluk de main thread blokkeert zonder code moeilijk leesbaar en schrijfbaar te maken.

In de volgende post zullen we onderzoeken hoe ze op Android passen om al het werk bij te houden dat je vanaf een scherm bent begonnen! Lees het eens:

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.