Sean McQuillan
Sean McQuillan

Follow

Apr 30, 2019 – 9 min read

Detta är en del av en serie i flera delar om hur man använder Coroutines på Android. Det här inlägget fokuserar på hur coroutines fungerar och vilka problem de löser.

Andra artiklar i den här serien:

Kotlin coroutines introducerar en ny stil av samtidighet som kan användas på Android för att förenkla asynkron kod. Även om de är nya för Kotlin i 1.3 har konceptet coroutines funnits sedan programmeringsspråkens gryning. Det första språket som utforskade användningen av coroutiner var Simula 1967.

Under de senaste åren har coroutiner ökat i popularitet och ingår nu i många populära programmeringsspråk som Javascript, C#, Python, Ruby och Go för att nämna några. Kotlin-coroutiner bygger på etablerade koncept som har använts för att bygga stora program.

På Android är coroutiner en bra lösning på två problem:

  1. Långkörande uppgifter är uppgifter som tar för lång tid att blockera huvudtråden.
  2. Main-safety gör att du kan se till att varje suspensionsfunktion kan anropas från huvudtråden.

Låt oss dyka ner i var och en av dem för att se hur coroutines kan hjälpa oss att strukturera koden på ett renare sätt!

Långkörande uppgifter

Att hämta en webbsida eller att interagera med ett API innebär båda att göra en nätverksförfrågan. På samma sätt innebär läsning från en databas eller laddning av en bild från en disk att en fil läses. Den här typen av saker är vad jag kallar långvariga uppgifter – uppgifter som tar alldeles för lång tid för att din app ska stanna och vänta på dem!

Det kan vara svårt att förstå hur snabbt en modern telefon exekverar kod jämfört med en nätverksförfrågan. På en Pixel 2 tar en enda CPU-cykel strax under 0,00000000000004 sekunder, en siffra som är ganska svår att förstå i mänskliga termer. Men om du tänker på en nätverksförfrågan som ett ögonblick, cirka 400 millisekunder (0,4 sekunder), är det lättare att förstå hur snabbt CPU:n arbetar. På ett ögonblick, eller en något långsam nätverksförfrågan, kan processorn utföra över en miljard cykler!

På Android har varje app en huvudtråd som ansvarar för att hantera användargränssnittet (t.ex. ritning av vyer) och samordna användarinteraktioner. Om det är för mycket arbete på den här tråden verkar appen hänga eller bli långsam, vilket leder till en oönskad användarupplevelse. Alla långvariga uppgifter bör utföras utan att blockera huvudtråden, så att din app inte visar vad som kallas ”jank”, som frusna animationer, eller reagerar långsamt på beröringshändelser.

För att utföra en nätverksförfrågan utanför huvudtråden är callbacks ett vanligt mönster. Callbacks ger ett handtag till ett bibliotek som det kan använda för att ringa tillbaka till din kod vid en framtida tidpunkt. Med callbacks kan hämtning av developer.android.com se ut så här:

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

Även om get anropas från huvudtråden kommer den att använda en annan tråd för att utföra nätverksbegäran. När resultatet sedan är tillgängligt från nätverket kommer callbacken att anropas i huvudtråden. Detta är ett utmärkt sätt att hantera långkörande uppgifter, och bibliotek som Retrofit kan hjälpa dig att göra nätverksförfrågningar utan att blockera huvudtråden.

Användning av coroutiner för långkörande uppgifter

Coroutiner är ett sätt att förenkla koden som används för att hantera långkörande uppgifter som fetchDocs. För att utforska hur coroutines gör koden för långkörande uppgifter enklare, låt oss skriva om callback-exemplet ovan för att använda coroutines.

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

Blockerar inte den här koden huvudtråden? Hur returnerar den ett resultat från get utan att vänta på nätverksförfrågan och blockera? Det visar sig att coroutines är ett sätt för Kotlin att utföra den här koden utan att blockera huvudtråden.

Coroutines bygger på vanliga funktioner genom att lägga till två nya operationer. Förutom invoke (eller call) och return lägger coroutines till suspend och resume.

  • suspend – pausa utförandet av den aktuella coroutinen och spara alla lokala variabler
  • resume – fortsätta en suspenderad coroutine från den plats där den pausades

Den här funktionaliteten läggs till av Kotlin genom nyckelordet suspend på funktionen. Du kan endast anropa suspend-funktioner från andra suspend-funktioner, eller genom att använda en coroutine builder som launch för att starta en ny coroutine.

Suspend och resume arbetar tillsammans för att ersätta callbacks.

I exemplet ovan kommer get att suspendera coroutinen innan den startar nätverksförfrågan. Funktionen get kommer fortfarande att vara ansvarig för att köra nätverksbegäran utanför huvudtråden. När nätverksbegäran avslutas kan den sedan, i stället för att anropa en callback för att meddela huvudtråden, helt enkelt återuppta den coroutine som den avbröt.

Animation som visar hur Kotlin implementerar suspend och resume för att ersätta callbacks.

Om man tittar på hur fetchDocs exekverar kan man se hur suspend fungerar. När en coroutine avbryts kopieras och sparas den aktuella stackramen (platsen som Kotlin använder för att hålla reda på vilken funktion som körs och dess variabler) och sparas för senare. När den återupptas kopieras stackramen tillbaka från platsen där den sparades och börjar köras igen. I mitten av animationen – när alla coroutines på huvudtråden har avbrutits – är huvudtråden fri att uppdatera skärmen och hantera användarhändelser. Tillsammans ersätter upphävande och återupptagande callbacks. Ganska snyggt!

När alla coroutines på huvudtråden har avbrutits är huvudtråden fri att göra annat arbete.

Även om vi skrev enkel sekventiell kod som ser ut exakt som en blockerande nätverksförfrågan, kommer coroutines att köra vår kod exakt som vi vill och undvika att blockera huvudtråden!

Nästan ska vi ta en titt på hur man använder coroutines för huvudtrådssäkerhet och utforska dispatchers.

Huvudtrådssäkerhet med coroutines

I Kotlin-coroutines är välskrivna suspensionsfunktioner alltid säkra att anropa från huvudtråden. Oavsett vad de gör ska de alltid tillåta alla trådar att anropa dem.

Men det finns många saker vi gör i våra Android-appar som är för långsamma för att ske på huvudtråden. Nätverksförfrågningar, analysering av JSON, läsning eller skrivning från databasen eller till och med bara iterering över stora listor. Alla dessa har potential att köras tillräckligt långsamt för att orsaka användarens synliga ”jank” och bör köras utanför huvudtråden.

Användning av suspend säger inte till Kotlin att köra en funktion på en bakgrundstråd. Det är värt att tydligt och ofta säga att coroutines ska köras på huvudtråden. Det är faktiskt en riktigt bra idé att använda Dispatchers.Main.immediate när du startar en coroutine som svar på en UI-händelse – på så sätt kan resultatet vara tillgängligt för användaren redan i nästa bildruta om det inte slutar med en långvarig uppgift som kräver huvudsäkerhet.

Coroutiner kommer att köras på huvudtråden, och suspendera betyder inte bakgrund.

För att göra en funktion som utför arbete som är för långsamt för huvudtråden huvudsäker, kan du tala om för Kotlin-coroutiner att utföra arbete på antingen Default eller IO dispatcher. I Kotlin måste alla coroutiner köras i en dispatcher – även när de körs på huvudtråden. Coroutiner kan suspendera sig själva och dispatcher är det som vet hur de ska återupptas.

För att specificera var coroutinerna ska köras tillhandahåller Kotlin tre Dispatchers som du kan använda för tråddistribution.

+-----------------------------------+
| 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 kommer att tillhandahålla huvudsäkerhet automatiskt om du använder suspend-funktioner, RxJava eller LiveData.

** Nätverksbibliotek som Retrofit och Volley hanterar sina egna trådar och kräver inte explicit huvudsäkerhet i din kod när de används med Kotlin-coroutiner.

För att fortsätta med exemplet ovan använder vi dispatchers för att definiera funktionen get. Inne i kroppen av get anropar du withContext(Dispatchers.IO) för att skapa ett block som kommer att köras på IO dispatcher. All kod som du lägger in i det blocket kommer alltid att exekveras på IO dispatcher. Eftersom withContext i sig själv är en suspenderande funktion kommer den att fungera med hjälp av coroutines för att ge huvudsäkerhet.

// 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 coroutines kan du göra tråddistribution med finkornig kontroll. Eftersom withContext låter dig styra vilken tråd en kodrad exekveras på utan att införa en callback för att returnera resultatet, kan du tillämpa det på mycket små funktioner som att läsa från din databas eller utföra en nätverksförfrågan. Så en bra metod är att använda withContext för att se till att varje funktion säkert kan anropas på vilken Dispatcher som helst, inklusive Main – på så sätt behöver anroparen aldrig tänka på vilken tråd som kommer att behövas för att utföra funktionen.

I det här exemplet exekveras fetchDocs på huvudtråden, men kan säkert anropa get som utför en nätverksförfrågan i bakgrunden. Eftersom coroutiner har stöd för att avbryta och återuppta kommer coroutinen på huvudtråden att återupptas med resultatet så snart withContext-blocket är klart.

Välskrivna avbrottsfunktioner är alltid säkra att anropa från huvudtråden (eller main-safe).

Det är en riktigt bra idé att göra varje avbrottsfunktion main-safe. Om den gör något som rör disken, nätverket eller till och med bara använder för mycket CPU, använd withContext för att göra den säker att anropa från huvudtråden. Detta är mönstret som coroutines-baserade bibliotek som Retrofit och Room följer. Om du följer den här stilen i hela din kodbas blir din kod mycket enklare och du undviker att blanda trådproblem med programlogik. När det följs konsekvent kan coroutines starta på huvudtråden och göra nätverks- eller databasförfrågningar med enkel kod och samtidigt garantera att användarna inte ser ”jank”.

Prestationen för withContext

withContext är lika snabb som callbacks eller RxJava när det gäller att ge huvudtråden säkerhet. Det är till och med möjligt att optimera withContext-anrop utöver vad som är möjligt med callbacks i vissa situationer. Om en funktion kommer att göra 10 anrop till en databas kan du säga till Kotlin att byta en gång i en yttre withContext runt alla 10 anrop. Då kommer databasbiblioteket visserligen att anropa withContext upprepade gånger, men det kommer att stanna på samma dispatcher och följa en snabb väg. Dessutom är växling mellan Dispatchers.Default och Dispatchers.IO optimerad för att undvika trådbyten när det är möjligt.

Vad händer härnäst

I det här inlägget utforskade vi vilka problem som coroutines är bra på att lösa. Coroutines är ett riktigt gammalt koncept i programmeringsspråk som har blivit populärt på senare tid på grund av deras förmåga att göra kod som interagerar med nätverket enklare.

På Android kan du använda dem för att lösa två riktigt vanliga problem:

  1. Simplifiera koden för långvariga uppgifter, t.ex. läsning från nätverket, disken eller till och med analysering av ett stort JSON-resultat.
  2. Uppföra exakt main-safety för att se till att du aldrig oavsiktligt blockerar huvudtråden utan att göra koden svår att läsa och skriva.

I nästa inlägg utforskar vi hur de passar in i Android för att hålla reda på allt arbete som du startade från en skärm! Läs det här:

Lämna ett svar

Din e-postadress kommer inte publiceras.