Sean McQuillan
Sean McQuillan

Follow

Apr 30, 2019 – 9 min citește

Acest articol face parte dintr-o serie de mai multe părți despre utilizarea Coroutines pe Android. Această postare se concentrează pe modul în care funcționează corutinele și pe problemele pe care le rezolvă.

Alte articole din această serie:

Corutinele Kotlin introduc un nou stil de concurrență care poate fi folosit pe Android pentru a simplifica codul asincron. Deși sunt noi în Kotlin în versiunea 1.3, conceptul de corutine a existat încă de la începuturile limbajelor de programare. Primul limbaj care a explorat utilizarea corutinelor a fost Simula în 1967.

În ultimii câțiva ani, corutinele au crescut în popularitate și sunt acum incluse în multe limbaje de programare populare, cum ar fi Javascript, C#, Python, Ruby și Go, pentru a numi doar câteva. Corutinele Kotlin se bazează pe concepte consacrate care au fost folosite pentru a construi aplicații mari.

Pe Android, corutinele sunt o soluție excelentă la două probleme:

  1. Sarcinile care rulează mult timp sunt sarcini care durează prea mult pentru a bloca firul principal.
  2. Main-safety vă permite să vă asigurați că orice funcție de suspendare poate fi apelată de pe firul principal.

Să ne scufundăm în fiecare dintre ele pentru a vedea cum corutinele ne pot ajuta să structurăm codul într-un mod mai curat!

Tașe de execuție îndelungată

Căutarea unei pagini web sau interacțiunea cu un API implică ambele efectuarea unei cereri de rețea. În mod similar, citirea dintr-o bază de date sau încărcarea unei imagini de pe disc implică citirea unui fișier. Aceste tipuri de lucruri sunt ceea ce eu numesc sarcini de lungă durată – sarcini care durează mult prea mult timp pentru ca aplicația dvs. să se oprească și să le aștepte!

Poate fi greu de înțeles cât de repede un telefon modern execută codul în comparație cu o cerere de rețea. Pe un Pixel 2, un singur ciclu CPU durează puțin sub 0,0000000004 secunde, un număr destul de greu de înțeles în termeni umani. Cu toate acestea, dacă te gândești la o solicitare de rețea ca la o clipită, în jur de 400 de milisecunde (0,4 secunde), este mai ușor să înțelegi cât de repede funcționează procesorul. Într-o clipită, sau într-o cerere de rețea oarecum lentă, procesorul poate executa peste un miliard de cicluri!

Pe Android, fiecare aplicație are un fir principal care se ocupă de gestionarea interfeței cu utilizatorul (cum ar fi desenarea vizualizărilor) și de coordonarea interacțiunilor cu utilizatorul. Dacă se lucrează prea mult pe acest fir, aplicația pare să se blocheze sau să încetinească, ceea ce duce la o experiență nedorită pentru utilizator. Orice sarcină care rulează mult timp ar trebui să fie efectuată fără a bloca firul principal, astfel încât aplicația dvs. să nu afișeze ceea ce se numește „jank”, cum ar fi animațiile înghețate, sau să răspundă lent la evenimentele de atingere.

Pentru a efectua o cerere de rețea în afara firului principal, un model comun este callback-ul. Callback-urile oferă un mâner către o bibliotecă pe care o poate folosi pentru a apela înapoi în codul dvs. la un moment dat. Cu callback-uri, preluarea developer.android.com ar putea arăta astfel:

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

Chiar dacă get este apelat din firul principal, acesta va folosi un alt fir pentru a efectua cererea de rețea. Apoi, odată ce rezultatul este disponibil din rețea, callback-ul va fi apelat pe firul principal. Aceasta este o modalitate foarte bună de a gestiona sarcinile de lungă durată, iar bibliotecile precum Retrofit vă pot ajuta să efectuați cereri de rețea fără a bloca firul principal.

Utilizarea corutinelor pentru sarcinile de lungă durată

Corutinele sunt o modalitate de a simplifica codul utilizat pentru a gestiona sarcinile de lungă durată precum fetchDocs. Pentru a explora modul în care corutinele simplifică codul pentru sarcinile cu execuție îndelungată, haideți să rescriem exemplul de callback de mai sus pentru a utiliza corutine.

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

Nu blochează acest cod firul principal? Cum returnează un rezultat de la get fără a aștepta cererea de rețea și fără a bloca? Se pare că corutinele oferă o modalitate pentru Kotlin de a executa acest cod și de a nu bloca niciodată firul principal.

Corutinele se bazează pe funcțiile obișnuite prin adăugarea a două noi operații. În plus față de invoke (sau call) și return, corutinele adaugă suspendarea și reluarea.

  • suspend – suspendă execuția corutinei curente, salvând toate variabilele locale
  • resume – continuă o corutină suspendată din locul în care a fost suspendată

Această funcționalitate este adăugată de Kotlin prin cuvântul cheie suspend din funcție. Puteți apela funcțiile de suspendare numai de la alte funcții de suspendare sau prin utilizarea unui constructor de corutine, cum ar fi launch, pentru a începe o nouă cortină.

Suspendarea și reluarea lucrează împreună pentru a înlocui callback-urile.

În exemplul de mai sus, get va suspenda corutina înainte de a începe solicitarea de rețea. Funcția get va fi în continuare responsabilă pentru rularea cererii de rețea în afara firului principal. Apoi, atunci când cererea de rețea se finalizează, în loc să apeleze un callback pentru a notifica firul principal, aceasta poate pur și simplu să reia corutina pe care a suspendat-o.

Animare care arată cum Kotlin implementează suspendarea și reluarea pentru a înlocui callback-urile.

Urmărind modul în care se execută fetchDocs, puteți vedea cum funcționează suspendarea. Ori de câte ori o corutină este suspendată, cadrul curent al stivei (locul pe care Kotlin îl folosește pentru a ține evidența funcției care se execută și a variabilelor sale) este copiat și salvat pentru mai târziu. Când se reia, cadrul stivei este copiat înapoi din locul în care a fost salvat și începe să ruleze din nou. În mijlocul animației – când toate corutinele de pe firul principal sunt suspendate, firul principal este liber să actualizeze ecranul și să gestioneze evenimentele utilizatorului. Împreună, suspendarea și reluarea înlocuiesc callback-urile. Destul de îngrijit!

Când toate corutinele de pe firul principal sunt suspendate, firul principal este liber să facă alte activități.

Chiar dacă am scris un cod secvențial simplu care arată exact ca o cerere de rețea care blochează, corutinele vor rula codul nostru exact așa cum dorim și vor evita blocarea firului principal!

În continuare, să aruncăm o privire asupra modului de utilizare a corutinelor pentru siguranța principală și să explorăm dispecerii.

Siguranța principală cu corutine

În corutinele Kotlin, funcțiile de suspendare bine scrise sunt întotdeauna sigure pentru a fi apelate din firul principal. Indiferent de ceea ce fac, ele ar trebui să permită întotdeauna oricărui fir de execuție să le apeleze.

Dar, există o mulțime de lucruri pe care le facem în aplicațiile noastre Android care sunt prea lente pentru a se întâmpla pe firul principal. Cererile de rețea, analiza JSON, citirea sau scrierea din baza de date sau chiar și simpla iterație peste liste mari. Oricare dintre acestea are potențialul de a rula suficient de lent pentru a provoca „jank” vizibil pentru utilizator și ar trebui să ruleze în afara firului principal.

Utilizarea suspend nu îi spune lui Kotlin să ruleze o funcție pe un fir de fundal. Merită să se spună clar și des că corutinele vor rula pe firul principal. De fapt, este o idee foarte bună să folosiți Dispatchers.Main.immediate atunci când lansați o coroutine ca răspuns la un eveniment UI – în acest fel, dacă nu ajungeți să faceți o sarcină de lungă durată care necesită siguranță principală, rezultatul poate fi disponibil chiar în următorul cadru pentru utilizator.

Corutinele vor rula pe firul principal, iar suspendarea nu înseamnă fundal.

Pentru a face o funcție care face o muncă prea lentă pentru firul principal main-safe, puteți spune corutinelor Kotlin să efectueze munca fie pe dispeceratul Default, fie pe cel IO. În Kotlin, toate corutinele trebuie să ruleze într-un dispecerat – chiar și atunci când rulează pe firul principal. Corutinele se pot suspenda singure, iar dispecerul este lucrul care știe cum să le reia.

Pentru a specifica unde ar trebui să ruleze corutinele, Kotlin oferă trei dispecerate pe care le puteți folosi pentru dispecerizarea firelor.

+-----------------------------------+
| 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 va asigura automat siguranța principală dacă folosiți funcții de suspendare, RxJava sau LiveData.

** Bibliotecile de rețea, cum ar fi Retrofit și Volley, își gestionează propriile fire de execuție și nu necesită siguranță principală explicită în codul dvs. atunci când sunt utilizate cu corutine Kotlin.

Pentru a continua cu exemplul de mai sus, să folosim dispeceratele pentru a defini funcția get. În interiorul corpului funcției get apelați withContext(Dispatchers.IO) pentru a crea un bloc care va rula pe dispecerul IO. Orice cod pe care îl puneți în interiorul acelui bloc se va executa întotdeauna pe dispecerul IO. Deoarece withContext este ea însăși o funcție de suspendare, va funcționa folosind corutine pentru a asigura siguranța principală.

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

Cu corutine puteți face dispecerizarea firelor cu un control fin. Deoarece withContext vă permite să controlați pe ce fir de execuție se execută orice linie de cod fără a introduce un callback pentru a returna rezultatul, îl puteți aplica la funcții foarte mici, cum ar fi citirea din baza de date sau efectuarea unei cereri de rețea. Așadar, o bună practică este să folosiți withContext pentru a vă asigura că fiecare funcție poate fi apelată în siguranță pe orice Dispatcher, inclusiv Main – în acest fel, apelantul nu trebuie să se gândească niciodată ce fir de execuție va fi necesar pentru a executa funcția.

În acest exemplu, fetchDocs se execută pe firul principal, dar poate apela în siguranță get care efectuează o cerere de rețea în fundal. Deoarece corutinele suportă suspendarea și reluarea, corutina de pe firul principal va fi reluată cu rezultatul imediat ce blocul withContext este finalizat.

Funcțiile de suspendare bine scrise sunt întotdeauna sigure pentru a fi apelate din firul principal (sau main-safe).

Este o idee foarte bună să faceți fiecare funcție de suspendare main-safe. Dacă face ceva care atinge discul, rețeaua sau chiar folosește prea mult CPU, folosiți withContext pentru a o face sigură pentru a fi apelată din firul principal. Acesta este modelul pe care îl urmează bibliotecile bazate pe corutine, cum ar fi Retrofit și Room. Dacă urmați acest stil în întreaga dvs. bază de cod, codul dvs. va fi mult mai simplu și veți evita amestecarea preocupărilor legate de threading cu logica aplicației. Atunci când sunt urmate în mod consecvent, corutinele sunt libere să se lanseze pe firul principal și să facă cereri de rețea sau de baze de date cu un cod simplu, garantând în același timp că utilizatorii nu vor vedea „jank.”

Performanța lui withContext

withContext este la fel de rapidă ca și callback-urile sau RxJava pentru asigurarea siguranței principale. Este chiar posibil să se optimizeze apelurile withContext dincolo de ceea ce este posibil cu callback-urile în anumite situații. Dacă o funcție va efectua 10 apeluri către o bază de date, îi puteți spune lui Kotlin să comute o dată într-un withContext exterior în jurul tuturor celor 10 apeluri. Apoi, chiar dacă biblioteca bazei de date va apela withContext în mod repetat, aceasta va rămâne pe același dispecerat și va urma o cale rapidă. În plus, comutarea între Dispatchers.Default și Dispatchers.IO este optimizată pentru a evita comutarea firelor de execuție ori de câte ori este posibil.

Ce urmează

În această postare am explorat care sunt problemele pe care corutinele le rezolvă excelent. Corutinele sunt un concept foarte vechi în limbajele de programare care au devenit populare recent datorită capacității lor de a face mai simplu codul care interacționează cu rețeaua.

Pe Android, le puteți folosi pentru a rezolva două probleme foarte comune:

  1. Simplificarea codului pentru sarcini de lungă durată, cum ar fi citirea din rețea, de pe disc sau chiar analizarea unui rezultat JSON mare.
  2. Realizarea unui main-safety precis pentru a vă asigura că nu blocați niciodată din greșeală firul principal fără a îngreuna citirea și scrierea codului.

În următoarea postare vom explora modul în care se potrivesc pe Android pentru a ține evidența tuturor lucrărilor pe care le-ați început de pe un ecran! Dați-i drumul la lectură:

Lasă un răspuns

Adresa ta de email nu va fi publicată.