Sean McQuillan
Sean McQuillan

Follow

Apr 30, 2019 – 9 min read

Jest to część wieloczęściowej serii o używaniu Coroutines na Androida. Ten post skupia się na tym, jak działają korutyny i jakie problemy rozwiązują.

Inne artykuły w tej serii:

Kotlin korutyny wprowadzają nowy styl współbieżności, który może być używany w systemie Android w celu uproszczenia kodu async. Chociaż są one nowe w Kotlinie w 1.3, koncepcja coroutines istnieje od zarania języków programowania. Pierwszym językiem, który zbadał użycie coroutines była Simula w 1967 roku.

W ciągu ostatnich kilku lat, coroutines zyskały na popularności i są teraz zawarte w wielu popularnych językach programowania, takich jak Javascript, C#, Python, Ruby i Go, aby wymienić tylko kilka. Kotlin coroutines są oparte na ustalonych koncepcjach, które zostały wykorzystane do budowania dużych aplikacji.

Na Androidzie coroutines są świetnym rozwiązaniem dwóch problemów:

  1. Długo działające zadania to zadania, które trwają zbyt długo, aby zablokować główny wątek.
  2. Main-safety pozwala zapewnić, że każda funkcja wstrzymania może być wywołana z głównego wątku.

Zanurzmy się w każdym z nich, aby zobaczyć, jak coroutines mogą pomóc nam ustrukturyzować kod w czystszy sposób!

Długotrwałe zadania

Pobieranie strony internetowej lub interakcja z interfejsem API wymagają wykonania żądania sieciowego. Podobnie, czytanie z bazy danych lub ładowanie obrazu z dysku wiąże się z czytaniem pliku. Tego typu rzeczy są tym, co nazywam długo działającymi zadaniami – zadaniami, które trwają o wiele za długo, aby Twoja aplikacja zatrzymała się i czekała na nie!

Może być trudno zrozumieć, jak szybko nowoczesny telefon wykonuje kod w porównaniu do żądania sieciowego. Na Pixel 2, pojedynczy cykl CPU trwa nieco poniżej 0.0000000004 sekund, liczba, która jest dość trudna do uchwycenia w kategoriach ludzkich. Jeśli jednak pomyśleć o żądaniu sieciowym jako o jednym mrugnięciu oka, czyli około 400 milisekund (0,4 sekundy), łatwiej zrozumieć, jak szybko działa procesor. W jednym mrugnięciu oka, lub nieco powolnym żądaniu sieciowym, procesor może wykonać ponad miliard cykli!

Na Androidzie, każda aplikacja ma główny wątek, który jest odpowiedzialny za obsługę UI (jak rysowanie widoków) i koordynowanie interakcji użytkownika. Jeśli na tym wątku dzieje się zbyt dużo pracy, aplikacja wydaje się zawieszać lub spowalniać, co prowadzi do niepożądanego doświadczenia użytkownika. Każde długo działające zadanie powinno być wykonywane bez blokowania głównego wątku, więc aplikacja nie wyświetla tego, co nazywa się „jank”, jak zamrożone animacje, lub reaguje powoli na zdarzenia dotykowe.

Aby wykonać żądanie sieciowe poza głównym wątkiem, powszechnym wzorcem są wywołania zwrotne. Wywołania zwrotne zapewniają uchwyt do biblioteki, który może być użyty do wywołania zwrotnego do twojego kodu w pewnym przyszłym czasie. Z wywołaniami zwrotnymi, pobieranie developer.android.com może wyglądać tak:

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

Mimo że get jest wywoływany z głównego wątku, użyje innego wątku do wykonania żądania sieciowego. Następnie, gdy wynik będzie dostępny z sieci, wywołanie zwrotne zostanie wywołane na głównym wątku. Jest to świetny sposób na obsługę długo działających zadań, a biblioteki takie jak Retrofit mogą pomóc w wykonywaniu żądań sieciowych bez blokowania głównego wątku.

Używanie coroutines do długo działających zadań

Coroutines są sposobem na uproszczenie kodu używanego do zarządzania długo działającymi zadaniami, takimi jak fetchDocs. Aby zbadać, w jaki sposób coroutines upraszczają kod dla długo działających zadań, przepiszmy powyższy przykład wywołania zwrotnego, aby użyć 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){/*...*/}

Czy ten kod nie blokuje głównego wątku? W jaki sposób zwraca on wynik z get bez czekania na żądanie sieci i blokowania? Okazuje się, że coroutines zapewniają Kotlinowi sposób na wykonanie tego kodu i nigdy nie blokują głównego wątku.

Coroutines opierają się na zwykłych funkcjach, dodając dwie nowe operacje. Oprócz wywołania (lub call) i powrotu, korutyny dodają zawieszenie i wznowienie.

  • suspend – wstrzymuje wykonywanie bieżącej korutyny, zachowując wszystkie zmienne lokalne
  • resume – kontynuuje zawieszoną korutynę od miejsca, w którym została wstrzymana

Ta funkcjonalność jest dodawana przez Kotlin przez słowo kluczowe suspend na funkcji. Możesz wywołać funkcje suspend tylko z innych funkcji suspend lub za pomocą konstruktora coroutine, takiego jak launch, aby rozpocząć nową coroutine.

Suspend i resume współpracują ze sobą, aby zastąpić callbacks.

W powyższym przykładzie get zawiesi coroutine, zanim rozpocznie żądanie sieciowe. Funkcja get nadal będzie odpowiedzialna za uruchomienie żądania sieciowego poza głównym wątkiem. Następnie, gdy żądanie sieciowe zakończy się, zamiast wywoływać wywołanie zwrotne, aby powiadomić główny wątek, może po prostu wznowić coroutine, który zawiesił.

Animacja pokazująca, jak Kotlin implementuje suspend i resume, aby zastąpić callbacki.

Patrząc na to, jak fetchDocs wykonuje się, można zobaczyć, jak działa suspend. Ilekroć coroutine jest zawieszony, bieżąca ramka stosu (miejsce, którego Kotlin używa do śledzenia, która funkcja jest uruchomiona i jej zmienne) jest kopiowana i zapisywana na później. Po wznowieniu, ramka stosu jest kopiowana z powrotem z miejsca, w którym została zapisana i zaczyna działać ponownie. W środku animacji – gdy wszystkie coroutines w głównym wątku są zawieszone, główny wątek jest wolny, aby aktualizować ekran i obsługiwać zdarzenia użytkownika. Razem, suspend i resume zastępują callbacki. Całkiem zgrabne!

Gdy wszystkie coroutines na głównym wątku są zawieszone, główny wątek jest wolny do wykonywania innej pracy.

Mimo że napisaliśmy prosty sekwencyjny kod, który wygląda dokładnie jak blokujące się żądanie sieciowe, coroutines uruchomią nasz kod dokładnie tak, jak chcemy i unikną blokowania głównego wątku!

Następnie przyjrzyjmy się, jak używać coroutines do main-safety i zbadajmy dispatchery.

Main-safety with coroutines

W Kotlinie coroutines, dobrze napisane funkcje zawieszające są zawsze bezpieczne do wywołania z głównego wątku. Bez względu na to, co robią, powinny zawsze pozwalać każdemu wątkowi na ich wywołanie.

Jest jednak wiele rzeczy, które robimy w naszych aplikacjach na Androida, które są zbyt wolne, aby wydarzyć się na głównym wątku. Żądania sieciowe, parsowanie JSON, czytanie lub pisanie z bazy danych, a nawet po prostu iterowanie nad dużymi listami. Każda z nich może działać na tyle wolno, że spowoduje widoczny dla użytkownika „jank” i powinna działać poza głównym wątkiem.

Użycie suspend nie mówi Kotlinowi, aby uruchomił funkcję na wątku tła. Warto powiedzieć jasno i często, że coroutines będą działać na głównym wątku. W rzeczywistości, naprawdę dobrym pomysłem jest użycie Dispatchers.Main.immediate podczas uruchamiania coroutine w odpowiedzi na zdarzenie UI – w ten sposób, jeśli nie skończysz wykonywać długo działającego zadania, które wymaga main-safety, wynik może być dostępny w następnej ramce dla użytkownika.

Koroutines będą działać na głównym wątku, a zawieszenie nie oznacza tła.

Aby uczynić funkcję wykonującą pracę, która jest zbyt wolna dla głównego wątku main-safe, możesz powiedzieć Kotlinowi coroutines, aby wykonywał pracę na dyspozytorze Default lub IO. W Kotlinie wszystkie coroutines muszą działać w dispatcherze – nawet jeśli działają na głównym wątku. Coroutines mogą się zawieszać, a dispatcher jest rzeczą, która wie, jak je wznowić.

Aby określić, gdzie coroutines powinny działać, Kotlin zapewnia trzy Dispatchery, których możesz użyć do wysyłania wątków.

+-----------------------------------+
| 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 zapewni main-safety automatycznie, jeśli używasz funkcji suspend, RxJava lub LiveData.

** Biblioteki sieciowe, takie jak Retrofit i Volley, zarządzają własnymi wątkami i nie wymagają jawnego main-safety w twoim kodzie, gdy są używane z Kotlin coroutines.

Aby kontynuować powyższy przykład, użyjmy dyspozytorów do zdefiniowania funkcji get. Wewnątrz ciała get wywołujemy withContext(Dispatchers.IO), aby utworzyć blok, który będzie działał na dyspozytorze IO. Każdy kod, który umieścisz wewnątrz tego bloku, zawsze będzie wykonywany na dyspozytorze IO. Ponieważ withContext jest sam w sobie funkcją zawieszającą, będzie działał przy użyciu coroutines, aby zapewnić główne bezpieczeństwo.

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

Dzięki coroutines możesz wykonać wysyłanie wątków z drobnoziarnistą kontrolą. Ponieważ withContext pozwala kontrolować, na którym wątku wykonuje się dowolny wiersz kodu bez wprowadzania wywołania zwrotnego, aby zwrócić wynik, można go zastosować do bardzo małych funkcji, takich jak czytanie z bazy danych lub wykonywanie żądania sieciowego. Tak więc dobrą praktyką jest użycie withContext, aby upewnić się, że każda funkcja może być bezpiecznie wywołana na dowolnym Dispatcher, w tym Main – w ten sposób osoba wywołująca nigdy nie musi się zastanawiać, jaki wątek będzie potrzebny do wykonania funkcji.

W tym przykładzie fetchDocs wykonuje się na głównym wątku, ale może bezpiecznie wywołać get, który wykonuje żądanie sieciowe w tle. Ponieważ korutyny obsługują zawieszanie i wznawianie, korutyna na głównym wątku zostanie wznowiona z wynikiem, gdy tylko blok withContext zostanie ukończony.

Dobrze napisane funkcje zawieszające są zawsze bezpieczne do wywoływania z głównego wątku (lub main-safe).

To naprawdę dobry pomysł, aby każda funkcja zawieszająca była main-safe. Jeśli robi cokolwiek, co dotyka dysku, sieci, lub nawet po prostu używa zbyt dużo CPU, użyj withContext, aby uczynić ją bezpieczną do wywołania z głównego wątku. Jest to wzór, który stosują biblioteki oparte na coroutines, takie jak Retrofit i Room. Jeśli będziesz podążał za tym stylem w całej swojej bazie kodu, Twój kod będzie znacznie prostszy i unikniesz mieszania problemów związanych z wątkami z logiką aplikacji. Gdy jest to konsekwentnie przestrzegane, coroutines mogą być uruchamiane na głównym wątku i wykonywać żądania sieciowe lub bazodanowe z prostym kodem, gwarantując jednocześnie, że użytkownicy nie zobaczą „jank”.”

Wydajność withContext

withContext jest tak szybka jak callbacks lub RxJava dla zapewnienia bezpieczeństwa głównego. W niektórych sytuacjach możliwe jest nawet zoptymalizowanie wywołań withContext poza to, co jest możliwe w przypadku callbacków. Jeśli funkcja wykona 10 wywołań do bazy danych, możesz powiedzieć Kotlinowi, aby przełączył się raz w zewnętrznym withContext wokół wszystkich 10 wywołań. Następnie, nawet jeśli biblioteka bazy danych będzie wywoływać withContext wielokrotnie, pozostanie na tym samym dyspozytorze i będzie podążać szybką ścieżką. Dodatkowo, przełączanie między Dispatchers.Default i Dispatchers.IO jest zoptymalizowane, aby uniknąć przełączania wątków, kiedy tylko jest to możliwe.

Co dalej

W tym poście zbadaliśmy, jakie problemy świetnie rozwiązują coroutiny. Korutyny są naprawdę starą koncepcją w językach programowania, które stały się popularne ostatnio ze względu na ich zdolność do upraszczania kodu, który współdziała z siecią.

Na Androidzie można ich użyć do rozwiązania dwóch naprawdę powszechnych problemów:

  1. Uproszczenie kodu dla długo działających zadań, takich jak czytanie z sieci, dysku lub nawet parsowanie dużego wyniku JSON.
  2. Wykonywanie precyzyjnego main-safety, aby zapewnić, że nigdy przypadkowo nie zablokujesz głównego wątku bez utrudniania czytania i pisania kodu.

W następnym poście zbadamy, jak pasują one do Androida, aby śledzić całą pracę, którą rozpocząłeś z ekranu! Przeczytaj to:

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.