Sean McQuillan
Sean McQuillan

Follow

30. apr, 2019 – 9 min read

Dette er en del af en serie i flere dele om brug af Coroutines på Android. Dette indlæg fokuserer på, hvordan coroutines fungerer, og hvilke problemer de løser.

Andre artikler i denne serie:

Kotlin-coroutines introducerer en ny stil for samtidighed, der kan bruges på Android til at forenkle asynkron kode. Selv om de er nye for Kotlin i 1.3, har begrebet coroutines været kendt siden programmeringssprogenes begyndelse. Det første sprog, der udforskede brugen af coroutiner, var Simula i 1967.

I de sidste par år er coroutiner vokset i popularitet og indgår nu i mange populære programmeringssprog som Javascript, C#, Python, Ruby og Go for blot at nævne nogle få. Kotlin-coroutiner er baseret på etablerede koncepter, der er blevet brugt til at opbygge store programmer.

På Android er coroutiner en god løsning på to problemer:

  1. Langtløbende opgaver er opgaver, der tager for lang tid til at blokere hovedtråden.
  2. Med main-safety kan du sikre, at enhver suspenderende funktion kan kaldes fra hovedtråden.

Lad os dykke ned i hver af dem for at se, hvordan coroutines kan hjælpe os med at strukturere kode på en renere måde!

Langtløbende opgaver

Hentning af en webside eller interaktion med en API involverer begge at foretage en netværksanmodning. På samme måde indebærer læsning fra en database eller indlæsning af et billede fra disken læsning af en fil. Den slags ting er det, jeg kalder langvarige opgaver – opgaver, der tager alt for lang tid til, at din app skal stoppe og vente på dem!

Det kan være svært at forstå, hvor hurtigt en moderne telefon eksekverer kode sammenlignet med en netværksanmodning. På en Pixel 2 tager en enkelt CPU-cyklus lige under 0,00000000000004 sekunder, hvilket er et tal, der er ret svært at forstå i menneskelige termer. Men hvis du tænker på en netværksanmodning som et enkelt blink med øjet, omkring 400 millisekunder (0,4 sekunder), er det lettere at forstå, hvor hurtigt CPU’en arbejder. På et enkelt øjenblink eller en noget langsom netværksanmodning kan CPU’en udføre over en milliard cyklusser!

På Android har alle apps en hovedtråd, der er ansvarlig for at håndtere brugergrænsefladen (f.eks. tegning af visninger) og koordinere brugerinteraktioner. Hvis der sker for meget arbejde på denne tråd, ser det ud til, at appen hænger eller bliver langsommere, hvilket fører til en uønsket brugeroplevelse. Enhver opgave, der kører længe, bør udføres uden at blokere hovedtråden, så din app ikke viser det, der kaldes “jank”, f.eks. frosne animationer, eller reagerer langsomt på berøringshændelser.

For at udføre en netværksanmodning uden for hovedtråden er callbacks et almindeligt mønster. Callbacks giver et håndtag til et bibliotek, som det kan bruge til at kalde tilbage i din kode på et senere tidspunkt. Med callbacks kan hentning af developer.android.com se således ud:

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

Selv om get kaldes fra hovedtråden, vil den bruge en anden tråd til at udføre netværksanmodningen. Når resultatet så er tilgængeligt fra netværket, kaldes callbacken i hovedtråden. Dette er en god måde at håndtere opgaver, der kører længe, og biblioteker som Retrofit kan hjælpe dig med at foretage netværksanmodninger uden at blokere hovedtråden.

Brug af coroutiner til opgaver, der kører længe

Coroutiner er en måde at forenkle den kode, der bruges til at håndtere opgaver, der kører længe, som fetchDocs. For at undersøge, hvordan coroutiner gør koden til opgaver, der kører længe, enklere, kan vi omskrive callback-eksemplet ovenfor, så det bruger coroutiner.

// 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){/*...*/}

Blokerer denne kode ikke hovedtråden? Hvordan returnerer den et resultat fra get uden at vente på netværksanmodningen og blokere? Det viser sig, at coroutiner giver Kotlin mulighed for at udføre denne kode uden at blokere hovedtråden.

Coroutiner bygger på almindelige funktioner ved at tilføje to nye operationer. Ud over invoke (eller call) og return tilføjer coroutines suspend og resume.

  • suspend – sætter udførelsen af den aktuelle coroutine på pause og gemmer alle lokale variabler
  • resume – fortsætter en suspenderet coroutine fra det sted, hvor den blev sat på pause

Denne funktionalitet tilføjes af Kotlin ved hjælp af nøgleordet suspend på funktionen. Du kan kun kalde suspend-funktioner fra andre suspend-funktioner eller ved at bruge en coroutine builder som launch til at starte en ny coroutine.

Suspend og resume arbejder sammen for at erstatte callbacks.

I eksemplet ovenfor vil get suspendere coroutinen, før den starter netværksanmodningen. Funktionen get vil stadig være ansvarlig for at køre netværksanmodningen fra hovedtråden. Når netværksanmodningen er afsluttet, kan den så i stedet for at kalde en callback for at underrette hovedtråden blot genoptage den coroutine, den har suspenderet, i stedet for at kalde en callback for at underrette hovedtråden.

Animation, der viser, hvordan Kotlin implementerer suspend og resume til at erstatte callbacks.

Hvis man ser på, hvordan fetchDocs udføres, kan man se, hvordan suspend fungerer. Hver gang en coroutine suspenderes, kopieres og gemmes den aktuelle stack frame (det sted, som Kotlin bruger til at holde styr på, hvilken funktion der kører og dens variabler) til senere brug. Når den genoptages, kopieres stakrammen tilbage fra det sted, hvor den blev gemt, og begynder at køre igen. Midt i animationen – når alle coroutinerne på hovedtråden er suspenderet – er hovedtråden fri til at opdatere skærmen og håndtere brugerbegivenheder. Sammen erstatter suspension og resume callbacks. Ret smart!

Når alle coroutinerne på hovedtråden er suspenderet, er hovedtråden fri til at udføre andet arbejde.

Selv om vi skrev ligefrem sekventiel kode, der ligner nøjagtigt en blokerende netværksanmodning, vil coroutinerne køre vores kode præcis, som vi ønsker, og undgå at blokere hovedtråden!

Næst skal vi se på, hvordan vi kan bruge coroutines til main-safety og udforske dispatchers.

Main-safety med coroutines

I Kotlin-coroutines er velskrevne suspend-funktioner altid sikre at kalde fra hovedtråden. Uanset hvad de gør, bør de altid tillade enhver tråd at kalde dem.

Men der er mange ting, vi gør i vores Android-apps, som er for langsomme til at ske på hovedtråden. Netværksanmodninger, parsing af JSON, læsning eller skrivning fra databasen eller endda bare iterering over store lister. Enhver af disse har potentiale til at køre langsomt nok til at forårsage bruger synlig “jank” og bør køre uden for hovedtråden.

Brug af suspend fortæller ikke Kotlin, at en funktion skal køres på en baggrundstråd. Det er værd at sige klart og ofte, at coroutiner skal køre på hovedtråden. Faktisk er det en rigtig god idé at bruge Dispatchers.Main.immediate, når du starter en coroutine som svar på en UI-hændelse – på den måde kan resultatet, hvis du ikke ender med at udføre en langvarig opgave, der kræver main-safety, være tilgængeligt for brugeren i den allernærmeste frame.

Coroutiner kører på hovedtråden, og suspendere betyder ikke baggrund.

For at gøre en funktion, der udfører arbejde, som er for langsomt for hovedtråden, main-safe, kan du fortælle Kotlin-coroutiner, at de skal udføre arbejde på enten Default eller IO dispatcher. I Kotlin skal alle coroutines køre i en dispatcher – også når de kører på hovedtråden. Coroutiner kan suspendere sig selv, og dispatcheren er det, der ved, hvordan de skal genoptages.

For at angive, hvor coroutinerne skal køre, tilbyder Kotlin tre dispatchere, som du kan bruge til tråddispatching.

+-----------------------------------+
| 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 sørger automatisk for main-safety, hvis du bruger suspend-funktioner, RxJava eller LiveData.

** Netværksbiblioteker som Retrofit og Volley administrerer deres egne tråde og kræver ikke eksplicit main-safety i din kode, når de bruges med Kotlin-coroutiner.

For at fortsætte med eksemplet ovenfor, skal vi bruge dispatchers til at definere get-funktionen. Inde i kroppen af get kalder du withContext(Dispatchers.IO) for at oprette en blok, der skal køre på IO dispatcheren. Enhver kode, som du lægger inden for denne blok, vil altid blive udført på IO dispatcheren. Da withContext i sig selv er en suspenderende funktion, vil den fungere ved hjælp af coroutiner for at give hovedsikkerhed.

// 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

Med coroutiner kan du lave tråddispatching med finkornet kontrol. Fordi withContext lader dig kontrollere, hvilken tråd en hvilken som helst kodelinje udføres på uden at indføre en callback til at returnere resultatet, kan du anvende det til meget små funktioner som f.eks. læsning fra din database eller udførelse af en netværksanmodning. Så en god praksis er at bruge withContext til at sikre, at enhver funktion kan kaldes sikkert på enhver Dispatcher, herunder Main – på den måde behøver den, der kalder funktionen, aldrig at tænke over, hvilken tråd der skal bruges til at udføre funktionen.

I dette eksempel udføres fetchDocs på hovedtråden, men kan sikkert kalde get, som udfører en netværksanmodning i baggrunden. Da coroutiner understøtter suspendere og genoptage, vil coroutinen på hovedtråden blive genoptaget med resultatet, så snart withContext-blokken er færdig.

Velskrevne suspenderende funktioner er altid sikre at kalde fra hovedtråden (eller main-safe).

Det er en rigtig god idé at gøre alle suspenderende funktioner main-safe. Hvis den gør noget, der berører disken, netværket eller bare bruger for meget CPU, skal du bruge withContext for at gøre den sikker at kalde fra hovedtråden. Dette er det mønster, som coroutines-baserede biblioteker som Retrofit og Room følger. Hvis du følger denne stil i hele din kodebase, vil din kode blive meget enklere og undgå at blande threadingproblemer med programlogik. Når det følges konsekvent, kan coroutiner frit starte på hovedtråden og foretage netværks- eller databaseanmodninger med simpel kode, samtidig med at brugerne garanteres ikke at se “jank.”

Performance af withContext

withContext er lige så hurtig som callbacks eller RxJava til at give hovedsikkerhed. Det er endda muligt at optimere withContext-opkald ud over, hvad der er muligt med callbacks i nogle situationer. Hvis en funktion skal foretage 10 kald til en database, kan du bede Kotlin om at skifte én gang i en ydre withContext omkring alle 10 kald. Så vil databasebiblioteket, selv om det vil kalde withContext gentagne gange, forblive på den samme dispatcher og følge en fast-path. Desuden er skift mellem Dispatchers.Default og Dispatchers.IO optimeret til at undgå trådskift, når det er muligt.

Hvad er det næste

I dette indlæg udforskede vi, hvilke problemer coroutines er gode til at løse. Coroutiner er et virkelig gammelt koncept i programmeringssprog, som er blevet populært på det seneste på grund af deres evne til at gøre kode, der interagerer med netværket, enklere.

På Android kan du bruge dem til at løse to virkelig almindelige problemer:

  1. Forenkling af koden til langvarige opgaver som f.eks. læsning fra netværket, disken eller endda parsing af et stort JSON-resultat.
  2. Udføre præcis main-safety for at sikre, at du aldrig ved et uheld blokerer hovedtråden uden at gøre koden vanskelig at læse og skrive.

I næste indlæg vil vi undersøge, hvordan de passer ind på Android for at holde styr på alt det arbejde, du startede fra en skærm! Giv det et læs:

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.