Sean McQuillan
Sean McQuillan

Follow

30.4, 2019 – 9 min read

Tämä on osa moniosaista sarjaa, joka käsittelee Coroutinesin käyttöä Androidissa. Tässä postauksessa keskitytään siihen, miten koroutiinit toimivat ja mitä ongelmia ne ratkaisevat.

Tämän sarjan muut artikkelit:

Kotlinin koroutiinit esittelevät uudenlaisen rinnakkaistoimintatavan, jota voidaan käyttää Androidissa yksinkertaistamaan asynkronista koodia. Vaikka ne ovat uusia Kotlinissa 1.3:ssa, coroutines-käsite on ollut olemassa ohjelmointikielten alkuajoista lähtien. Ensimmäinen kieli, jossa tutkittiin korutiinien käyttöä, oli Simula vuonna 1967.

Viime vuosina korutiinit ovat kasvattaneet suosiotaan, ja ne sisältyvät nyt moniin suosittuihin ohjelmointikieliin, kuten Javascriptiin, C#:iin, Pythoniin, Rubyyn ja Go:hon muutamia mainitakseni. Kotlinin coroutiinit perustuvat vakiintuneisiin konsepteihin, joita on käytetty suurten sovellusten rakentamiseen.

Androidissa coroutiinit ovat loistava ratkaisu kahteen ongelmaan:

  1. Pitkään käynnissä olevat tehtävät ovat tehtäviä, jotka kestävät liian kauan ja estävät pääsäikeen.
  2. Main-safety mahdollistaa sen varmistamisen, että mitä tahansa keskeytysfunktiota voidaan kutsua pääsäikeestä.

Sukelletaanpa kumpaankin, jotta nähdään, miten coroutiinit voivat auttaa meitä jäsentämään koodia siistimmin!

Pitkään käynnissä olevat tehtävät

Verkkosivun noutaminen tai vuorovaikutus sovellusrajapintakoodin (API:n) kanssa edellyttävät kummassakin tapauksessa verkon kautta tehtävän pyynnön tekemistä. Samoin tietokannasta lukeminen tai kuvan lataaminen levyltä edellyttää tiedoston lukemista. Tällaisia asioita kutsun pitkäkestoisiksi tehtäviksi – tehtäviksi, jotka kestävät aivan liian kauan, jotta sovelluksesi pysähtyisi odottamaan niitä!

Voi olla vaikea ymmärtää, kuinka nopeasti nykyaikainen puhelin suorittaa koodia verrattuna verkkopyyntöön. Pixel 2:ssa yksi suorittimen sykli kestää hieman alle 0,0000000004 sekuntia, mikä on ihmisen kannalta melko vaikeasti hahmotettava luku. Jos kuitenkin ajatellaan, että verkkopyyntö on yksi silmänräpäys, noin 400 millisekuntia (0,4 sekuntia), on helpompi ymmärtää, kuinka nopeasti suoritin toimii. Yhdessä silmänräpäyksessä eli hieman hitaassa verkkopyynnössä CPU voi suorittaa yli miljardi sykliä!

Androidissa jokaisella sovelluksella on pääsäie, joka vastaa käyttöliittymän käsittelystä (kuten näkymien piirtämisestä) ja käyttäjän vuorovaikutuksen koordinoinnista. Jos tässä säikeessä on liikaa työtä, sovellus näyttää roikkuvan tai hidastuvan, mikä johtaa epätoivottuun käyttökokemukseen. Kaikki pitkäkestoiset tehtävät tulisi tehdä estämättä pääsäiettä, jotta sovelluksesi ei näytä niin sanottua ”jankkia”, kuten pysähtyneitä animaatioita, tai reagoi hitaasti kosketustapahtumiin.

Verkkopyynnön suorittamiseksi pääsäikeen ulkopuolella yleinen malli on takaisinkutsut. Callbackit tarjoavat kahvan kirjastolle, jota se voi käyttää kutsuakseen takaisin koodiisi joskus tulevaisuudessa. Callbackien avulla developer.android.com:n hakeminen voisi näyttää tältä:

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

Vaikka get kutsutaan pääsäikeestä, se käyttää toista säiettä verkkopyynnön suorittamiseen. Sitten, kun tulos on saatavilla verkosta, takaisinkutsua kutsutaan pääsäikeessä. Tämä on hyvä tapa käsitellä pitkäkestoisia tehtäviä, ja Retrofitin kaltaiset kirjastot voivat auttaa sinua tekemään verkkopyyntöjä estämättä pääsäiettä.

Korutiinien käyttäminen pitkäkestoisiin tehtäviin

Korutiinit ovat tapa yksinkertaistaa koodia, jota käytetään pitkäkestoisten tehtävien, kuten fetchDocs, hallintaan. Tutkiaksemme, miten korutiineilla yksinkertaistetaan pitkäkestoisten tehtävien koodia, kirjoitetaan yllä oleva takaisinsoittoesimerkki uudelleen käyttämään korutiineja.

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

Eikö tämä koodi estä pääsäiettä? Miten se palauttaa tuloksen get odottamatta verkkopyyntöä ja blokkaamatta? Kävi ilmi, että coroutiinit tarjoavat Kotlinille tavan suorittaa tätä koodia eikä koskaan blokata pääsäiettä.

Coroutiinit perustuvat tavallisiin funktioihin lisäämällä niihin kaksi uutta operaatiota. Kutsun (tai kutsun) ja paluun lisäksi korutiineihin lisätään suspend ja resume.

  • suspend – keskeyttää nykyisen korutiinin suorituksen tallentaen kaikki paikalliset muuttujat
  • resume – jatkaa keskeytynyttä korutiineja paikasta, johon se keskeytettiin

Tämän toiminnallisuuden Kotlin lisää funktioon avainsanalla suspend. Keskeytysfunktioita voi kutsua vain toisista keskeytysfunktioista tai käyttämällä koroutiinien rakentajaa, kuten launch, uuden koroutiinin käynnistämiseen.

Keskeytys ja jatkaminen toimivat yhdessä korvaten takaisinkutsut.

Yllä olevassa esimerkissä get keskeyttää koroutiinin ennen kuin se aloittaa verkkopyynnön. Funktio get on edelleen vastuussa verkkopyynnön suorittamisesta pääsäikeestä. Kun verkkopyyntö on valmis, se voi sen sijaan, että se kutsuisi takaisinkutsua ilmoittaakseen siitä pääsäikeelle, yksinkertaisesti jatkaa keskeyttämäänsä korutiinia.

Animaatio, jossa näytetään, miten Kotlin toteuttaa keskeytyksen ja jatkamisen callbackien tilalle.

Katsomalla, miten fetchDocs suoritetaan, näet, miten keskeytys toimii. Aina kun korutiini keskeytetään, nykyinen pinokehys (paikka, jota Kotlin käyttää pitämään kirjaa siitä, mikä funktio on käynnissä ja sen muuttujista) kopioidaan ja tallennetaan myöhempää käyttöä varten. Kun se jatkuu, pinokehys kopioidaan takaisin sieltä, mihin se oli tallennettu, ja se alkaa taas toimia. Kesken animaation – kun kaikki pääsäikeen coroutiinit on keskeytetty – pääsäie on vapaa päivittämään näyttöä ja käsittelemään käyttäjän tapahtumia. Yhdessä keskeyttäminen ja jatkaminen korvaavat callbackit. Aika siistiä!

Kun kaikki pääsäikeen coroutiinit on keskeytetty, pääsäie on vapaa tekemään muuta työtä.

Vaikka kirjoitimme suoraviivaista peräkkäistä koodia, joka näyttää täsmälleen samalta kuin estävä verkkopyyntö, coroutiinit suorittavat koodimme täsmälleen haluamallamme tavalla ja välttävät pääsäikeen estämisen!

Katsotaan seuraavaksi, miten coroutiineja voi käyttää pääturvallisuuteen ja tutustutaan dispatchereihin.

Pääturvallisuutta coroutiineilla

Kotlinin coroutiineissa hyvin kirjoitettuja keskeytysfunktioita on aina turvallista kutsua pääsäikeestä. Riippumatta siitä, mitä ne tekevät, niiden pitäisi aina sallia minkä tahansa säikeen kutsua niitä.

Mutta Android-sovelluksissamme tehdään paljon asioita, jotka ovat liian hitaita tapahtumaan pääsäikeessä. Verkkopyynnöt, JSONin jäsentäminen, tietokannan lukeminen tai kirjoittaminen, tai vaikka vain iterointi suurten listojen yli. Mikä tahansa näistä voi toimia tarpeeksi hitaasti aiheuttaakseen käyttäjälle näkyvää ”jankkausta”, ja ne pitäisi ajaa pääsäikeessä.

Käyttämällä suspend ei käsketä Kotlinia ajamaan funktiota taustasäikeessä. Kannattaa sanoa selvästi ja usein, että korutiinit suoritetaan pääsäikeessä. Itse asiassa on todella hyvä idea käyttää Dispatchers.Main.immediate, kun käynnistät coroutinin vastauksena UI-tapahtumaan – näin, jos et päädy tekemään pitkäkestoista tehtävää, joka vaatii pääkierreturvallisuutta, lopputulos voi olla käyttäjän käytettävissä heti seuraavassa kehyksessä.

Korutiineja suoritetaan pääsäikeessä, eikä keskeyttäminen tarkoita taustaa.

Tehdäksesi funktiosta, joka tekee työtä, joka on liian hidas pääsäikeelle, voit käskeä Kotlin-korutiineja suorittamaan työn joko Default– tai IO-dispatcherissa. Kotlinissa kaikkien coroutinien on suoritettava dispatcherissa – myös silloin, kun ne suoritetaan pääsäikeessä. Coroutiinit voivat keskeyttää itsensä, ja dispatcher on se, joka osaa jatkaa niitä.

Määrittääksesi, missä coroutiineja pitäisi ajaa, Kotlin tarjoaa kolme dispatcheria, joita voit käyttää säikeen dispatcherina.

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

* Kotlin tarjoaa pääsuunnan turvallisuuden automaattisesti, jos käytät suspend-funktioita, RxJavaa tai LiveDataa.

** Verkkokirjastot, kuten Retrofit ja Volley, hallinnoivat omia säikeitään eivätkä vaadi eksplisiittistä main-safetyä koodissasi, kun niitä käytetään Kotlin-korutiinien kanssa.

Jatkaaksemme yllä olevaan esimerkkiin, käytämme dispatchereita määrittelemään get-funktion. get:n rungon sisällä kutsutaan withContext(Dispatchers.IO) luomaan lohko, joka suoritetaan IO-dispatcherilla. Kaikki tuon lohkon sisällä oleva koodi suoritetaan aina IO-dispatcherilla. Koska withContext on itsessään keskeytysfunktio, se toimii koroutiineja käyttäen pääturvallisuuden tarjoamiseksi.

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

Koroutiineilla voit tehdä säikeiden välityksen hienojakoisella hallinnalla. Koska withContext:n avulla voit kontrolloida, missä säikeessä mikä tahansa koodirivi suoritetaan ottamatta käyttöön takaisinkutsua tuloksen palauttamiseksi, voit soveltaa sitä hyvin pieniin toimintoihin, kuten tietokannasta lukemiseen tai verkkopyynnön suorittamiseen. Hyvä käytäntö on siis käyttää withContext:tä varmistaaksesi, että jokaista funktiota on turvallista kutsua millä tahansa Dispatcher:llä, myös Main:llä – näin kutsujan ei tarvitse koskaan miettiä, mitä säiettä tarvitaan funktion suorittamiseen.

Tässä esimerkissä fetchDocs suoritetaan pääsäikeessä, mutta se voi turvallisesti kutsua get:tä, joka suorittaa verkkopyynnön taustalla. Koska koroutiinit tukevat keskeyttämistä ja jatkamista, pääsäikeessä olevaa koroutiinia jatketaan tuloksen kanssa heti, kun withContext-lohko on suoritettu loppuun.

Hyvin kirjoitettuja keskeytysfunktioita on aina turvallista kutsua pääsäikeestä (tai main-safe).

Kaikkien keskeytysfunktioiden kannattaa tehdä main-safe. Jos se tekee jotain sellaista, joka koskettaa levyä, verkkoa tai vain käyttää liikaa prosessoria, käytä withContext tehdäksesi siitä turvallisen kutsuttavaksi pääsäikeestä. Tätä mallia noudattavat koroutiineihin perustuvat kirjastot, kuten Retrofit ja Room. Jos noudatat tätä tyyliä koko koodipohjassasi, koodistasi tulee paljon yksinkertaisempaa ja vältyt sekoittamasta säikeistämiseen liittyviä huolenaiheita sovelluslogiikkaan. Kun sitä noudatetaan johdonmukaisesti, coroutiinit voivat vapaasti käynnistyä pääsäikeessä ja tehdä verkko- tai tietokantapyyntöjä yksinkertaisella koodilla ja samalla taata, etteivät käyttäjät näe ”jankkausta”.

WithContextin suorituskyky

withContext on yhtä nopea kuin callbackit tai RxJava pääturvallisuuden tarjoamisessa. Joissakin tilanteissa on jopa mahdollista optimoida withContext-kutsuja enemmän kuin mitä callbackien avulla on mahdollista. Jos funktio tekee 10 kutsua tietokantaan, voit käskeä Kotlinia vaihtamaan kerran ulomman withContext:n kaikkien 10 kutsun ympärille. Silloin, vaikka tietokantakirjasto kutsuu withContext toistuvasti, se pysyy samassa dispatcherissa ja noudattaa nopeaa polkua. Lisäksi vaihtaminen Dispatchers.Default:n ja Dispatchers.IO:n välillä on optimoitu välttämään säikeenvaihtoja aina kun mahdollista.

Mitä seuraavaksi

Tässä postauksessa tutkimme, millaisia ongelmia korutiineilla on hyvä ratkaista. Coroutiinit ovat todella vanha käsite ohjelmointikielissä, jotka ovat viime aikoina tulleet suosituiksi niiden kyvyn vuoksi tehdä verkon kanssa vuorovaikutuksessa olevasta koodista yksinkertaisempaa.

Androidissa voit käyttää niitä kahden todella tavallisen ongelman ratkaisemiseen:

  1. Pitkään suoritettavien tehtävien, kuten verkosta tai levyltä lukemisen tai jopa suuren JSON-tuloksen jäsentämisen, koodin yksinkertaistaminen.
  2. Tarkan main-safety-toiminnon suorittaminen sen varmistamiseksi, ettet koskaan vahingossakaan blokkaa pääsäiettä tekemättä koodista vaikeasti luettavaa ja kirjoitettavaa.

Seuraavassa postauksessa selvitämme, miten ne sopivat Androidiin, jotta voit pitää kirjaa kaikesta aloittamastasi työstä näytöltä! Käy lukemassa:

Vastaa

Sähköpostiosoitettasi ei julkaista.