Korutyny Kotlin umożliwiają pisanie przejrzystego, uproszczonego kodu asynchronicznego, który zapewnia responsywność aplikacji podczas zarządzania długotrwałymi zadaniami, takimi jak wywołania sieciowe czy operacje na dysku.
W tym artykule znajdziesz szczegółowe informacje o korutynach na Androidzie. Jeśli nie znasz współprogramów, przed przeczytaniem tego artykułu zapoznaj się z artykułem Współprogramy Kotlin na Androidzie.
Zarządzanie długotrwałymi zadaniami
Korutyny są rozszerzeniem zwykłych funkcji, które dodają 2 operacje do obsługi długotrwałych zadań. Oprócz invoke (lub call) i return korutyny dodają suspend i resume:
suspendwstrzymuje wykonywanie bieżącego współprogramu, zapisując wszystkie zmienne lokalne.resumewznawia wykonywanie zawieszonej korutyny od miejsca, w którym została zawieszona.
Funkcje suspend można wywoływać tylko z innych funkcji suspend lub za pomocą konstruktora współprogramów, np. launch, aby uruchomić nowy współprogram.
Poniższy przykład pokazuje prostą implementację współprogramu dla hipotetycznego długotrwałego zadania:
suspend fun fetchDocs() { // Dispatchers.Main val result = get("https://developer.android.com") // Dispatchers.IO for `get` show(result) // Dispatchers.Main } suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
W tym przykładzie get() nadal działa w głównym wątku, ale zawiesza współprogram przed rozpoczęciem żądania sieciowego. Gdy żądanie sieciowe zostanie zrealizowane, get wznawia zawieszony współprogram zamiast używać wywołania zwrotnego do powiadamiania wątku głównego.
Kotlin używa ramki stosu do zarządzania tym, która funkcja jest uruchomiona, oraz zmiennymi lokalnymi. Podczas wstrzymywania korutyny bieżąca ramka stosu jest kopiowana i zapisywana na później. Po wznowieniu ramka stosu jest kopiowana z powrotem z miejsca, w którym została zapisana, a funkcja zaczyna ponownie działać. Chociaż kod może wyglądać jak zwykłe sekwencyjne żądanie blokujące, współprogram zapewnia, że żądanie sieciowe nie blokuje wątku głównego.
Używanie korutyn w celu zapewnienia bezpieczeństwa wątku głównego
Korutyny Kotlin używają mechanizmów dispatcher do określania, które wątki są używane do wykonywania współprogramów. Aby uruchomić kod poza wątkiem głównym, możesz poinformować korutyny Kotlin, aby wykonywały pracę na dyspozytorze Default lub IO. W Kotlinie wszystkie współprogramy muszą działać w dispatcherze, nawet jeśli są uruchamiane w wątku głównym. Korutyny mogą się zawieszać, a za ich wznowienie odpowiada dyspozytor.
Aby określić, gdzie mają być uruchamiane współprogramy, Kotlin udostępnia 3 dyspozytorów:
- Dispatchers.Main – użyj tego dyspozytora, aby uruchomić współprogram w głównym wątku Androida. Należy go używać tylko do interakcji z interfejsem i wykonywania szybkich zadań. Może to być na przykład wywoływanie funkcji
suspend, wykonywanie operacji w ramach platformy interfejsu Androida czy aktualizowanie obiektówLiveData. - Dispatchers.IO – ten dyspozytor jest zoptymalizowany pod kątem wykonywania operacji wejścia-wyjścia na dysku lub w sieci poza wątkiem głównym. Może to być na przykład używanie komponentu Room, odczytywanie lub zapisywanie plików oraz wykonywanie operacji sieciowych.
- Dispatchers.Default – ten dyspozytor jest zoptymalizowany pod kątem wykonywania zadań wymagających dużej mocy obliczeniowej procesora poza wątkiem głównym. Przykładowe przypadki użycia to sortowanie listy i analizowanie formatu JSON.
Kontynuując poprzedni przykład, możesz użyć dyspozytorów, aby ponownie zdefiniować funkcję get. W treści funkcji get wywołaj funkcję withContext(Dispatchers.IO), aby utworzyć blok, który będzie działać w puli wątków wejścia/wyjścia. Każdy kod umieszczony w tym bloku jest zawsze wykonywany przez dyspozytora IO. Ponieważ withContext jest funkcją zawieszającą, funkcja get również jest funkcją zawieszającą.
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 (main-safety block) /* perform network IO here */ // Dispatchers.IO (main-safety block) } // Dispatchers.Main
Za pomocą korutyn możesz wysyłać wątki z dokładną kontrolą. Ponieważ withContext() umożliwia kontrolowanie puli wątków dowolnego wiersza kodu bez wprowadzania wywołań zwrotnych, możesz zastosować ją do bardzo małych funkcji, takich jak odczytywanie danych z bazy danych lub wykonywanie żądania sieciowego. Zalecamy używanie withContext(), aby mieć pewność, że każda funkcja jest bezpieczna dla wątku głównego, co oznacza, że możesz wywoływać ją z wątku głównego. Dzięki temu osoba wywołująca nigdy nie musi się zastanawiać, którego wątku należy użyć do wykonania funkcji.
W poprzednim przykładzie funkcja fetchDocs() jest wykonywana w wątku głównym, ale może bezpiecznie wywoływać funkcję get, która wykonuje żądanie sieciowe w tle.
Ponieważ współprogramy obsługują suspend i resume, współprogram w głównym wątku jest wznawiany z wynikiem get, gdy tylko blok withContext zostanie wykonany.
Wydajność funkcji withContext()
withContext() nie powoduje dodatkowego obciążenia w porównaniu z odpowiednią implementacją opartą na wywołaniach zwrotnych. W niektórych sytuacjach można też zoptymalizować połączenia withContext() w sposób wykraczający poza równoważną implementację opartą na wywołaniu zwrotnym. Jeśli na przykład funkcja wykonuje 10 wywołań sieciowych, możesz polecić Kotlinowi przełączanie wątków tylko raz, używając zewnętrznego withContext(). Dzięki temu, nawet jeśli biblioteka sieciowa używa withContext() wiele razy, pozostaje w tym samym dyspozytorze i nie przełącza wątków. Dodatkowo Kotlin optymalizuje przełączanie między Dispatchers.Default a Dispatchers.IO, aby w miarę możliwości unikać przełączania wątków.
Uruchamianie współprogramu
Korutyny można uruchamiać na 2 sposoby:
launch– uruchamia nowy współprogram i nie zwraca wyniku do elementu wywołującego. Każde zadanie, które można uznać za „zrobione i zapomniane”, można rozpocząć za pomocąlaunch.asyncrozpoczyna nowy współprogram i umożliwia zwrócenie wyniku za pomocą funkcji zawieszania o nazwieawait.
Zwykle należy launch nowy współprogram z funkcji regularnej, ponieważ funkcja regularna nie może wywoływać funkcji await. Używaj funkcji async tylko w innej korutynie lub w funkcji zawieszającej, gdy wykonujesz dekompozycję równoległą.
Równoległa dekompozycja
Wszystkie korutyny uruchomione w funkcji suspend muszą zostać zatrzymane, gdy ta funkcja zwróci wartość, więc prawdopodobnie musisz zagwarantować, że te korutyny zakończą się przed zwróceniem wartości. Dzięki strukturalnemu współbieżnemu wykonywaniu w Kotlinie możesz zdefiniować coroutineScope, które uruchamia co najmniej 1 korutynę. Następnie za pomocą funkcji await() (w przypadku jednego współprogramu) lub awaitAll() (w przypadku wielu współprogramów) możesz zagwarantować, że te współprogramy zakończą się przed powrotem z funkcji.
Zdefiniujmy na przykład element coroutineScope, który asynchronicznie pobiera 2 dokumenty. Wywołując await() w przypadku każdego odroczonego odwołania, gwarantujemy, że obie operacje async zakończą się przed zwróceniem wartości:
suspend fun fetchTwoDocs() = coroutineScope { val deferredOne = async { fetchDoc(1) } val deferredTwo = async { fetchDoc(2) } deferredOne.await() deferredTwo.await() }
Możesz też używać awaitAll() w kolekcjach, jak pokazano w tym przykładzie:
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main) coroutineScope { val deferreds = listOf( // fetch two docs at the same time async { fetchDoc(1) }, // async returns a result for the first doc async { fetchDoc(2) } // async returns a result for the second doc ) deferreds.awaitAll() // use awaitAll to wait for both network requests }
Mimo że funkcja fetchTwoDocs() uruchamia nowe korutyny za pomocą async, używa awaitAll(), aby poczekać na zakończenie tych uruchomionych korutyn przed zwróceniem wartości. Pamiętaj jednak, że nawet gdybyśmy nie wywołali funkcji awaitAll(), narzędzie do tworzenia coroutineScope nie wznowi korutyny, która wywołała funkcję fetchTwoDocs, dopóki wszystkie nowe korutyny nie zostaną ukończone.
Dodatkowo coroutineScope przechwytuje wszystkie wyjątki zgłaszane przez korutyny i przekazuje je z powrotem do elementu wywołującego.
Więcej informacji o równoległym rozkładzie znajdziesz w artykule Tworzenie funkcji zawieszających.
Pojęcia związane z współprogramami
CoroutineScope
CoroutineScope śledzi wszystkie utworzone przez siebie współprogramy za pomocą funkcji launch lub async. Trwającą pracę (czyli działające korutyny) można anulować w dowolnym momencie, wywołując scope.cancel(). W Androidzie niektóre biblioteki KTX udostępniają własny poziom CoroutineScope dla niektórych klas cyklu życia. Na przykład ViewModel ma viewModelScope, a Lifecycle ma lifecycleScope.
W przeciwieństwie do dyspozytora CoroutineScope nie uruchamia jednak korutyn.
Symbol viewModelScope jest też używany w przykładach w artykule Wątki w tle na Androidzie z użyciem współprogramów.
Jeśli jednak chcesz utworzyć własny CoroutineScope, aby kontrolować cykl życia korutyn w określonej warstwie aplikacji, możesz to zrobić w ten sposób:
class ExampleClass { // Job and Dispatcher are combined into a CoroutineContext which // will be discussed shortly val scope = CoroutineScope(Job() + Dispatchers.Main) fun exampleMethod() { // Starts a new coroutine within the scope scope.launch { // New coroutine that can call suspend functions fetchDocs() } } fun cleanUp() { // Cancel the scope to cancel ongoing coroutines work scope.cancel() } }
Anulowany zakres nie może tworzyć więcej korutyn. Dlatego wywołuj funkcję scope.cancel() tylko wtedy, gdy klasa, która kontroluje jej cykl życia, jest niszczona. Gdy używasz viewModelScope, klasa ViewModel automatycznie anuluje zakres w metodzie onCleared() klasy ViewModel.
Zadanie
Job to uchwyt do współprogramu. Każdy współprogram, który utworzysz za pomocą launch lub async, zwraca instancję Job, która jednoznacznie identyfikuje współprogram i zarządza jego cyklem życia. Możesz też przekazać Job do CoroutineScope, aby dalej zarządzać jego cyklem życia, jak pokazano w tym przykładzie:
class ExampleClass { // ... fun exampleMethod() { // Handle to the coroutine, you can control its lifecycle val job = scope.launch { // New coroutine } if (condition) { // Cancel the coroutine started above, this doesn't affect the scope // this coroutine was launched in job.cancel() } } }
CoroutineContext
CoroutineContext
określa zachowanie współprogramu za pomocą tego zestawu elementów:
Job: kontroluje cykl życia korutyny.CoroutineDispatcher: wysyła zadania do odpowiedniego wątku.CoroutineName: nazwa współprogramu, przydatna do debugowania.CoroutineExceptionHandler: obsługuje nieprzechwycone wyjątki.
W przypadku nowych współprogramów utworzonych w zakresie do nowego współprogramu przypisywana jest nowa instancja Job, a pozostałe elementy CoroutineContext są dziedziczone z zakresu zawierającego. Możesz zastąpić odziedziczone elementy, przekazując nowy element CoroutineContext do funkcji launch lub async. Pamiętaj, że przekazanie Job do launch lub async nie ma wpływu, ponieważ do nowej korutyny zawsze przypisywana jest nowa instancja Job.
class ExampleClass { val scope = CoroutineScope(Job() + Dispatchers.Main) fun exampleMethod() { // Starts a new coroutine on Dispatchers.Main as it's the scope's default val job1 = scope.launch { // New coroutine with CoroutineName = "coroutine" (default) } // Starts a new coroutine on Dispatchers.Default val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) { // New coroutine with CoroutineName = "BackgroundCoroutine" (overridden) } } }
Dodatkowe materiały o korutynach
Więcej informacji o korutynach znajdziesz w tych artykułach: