Współprogramy 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 szczegółowo omawiamy współprogramy 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
Współprogramy rozszerzają zwykłe funkcje, dodając 2 operacje do obsługi długotrwałych zadań. Oprócz invoke (lub call) i return współprogramy dodają suspend i resume:
suspendwstrzymuje wykonywanie bieżącego współprogramu, zapisując wszystkie zmienne lokalne.resumewznawia wykonywanie wstrzymanego współprogramu od miejsca, w którym został wstrzymany.
Funkcje suspend możesz wywoływać tylko z innych funkcji suspend lub za pomocą narzędzia do tworzenia współprogramów, takiego jak 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 funkcja get() nadal działa w wątku głównym, ale wstrzymuje współprogram przed rozpoczęciem żądania sieciowego. Gdy żądanie sieciowe zostanie zrealizowane, funkcja get wznawia wstrzymany współprogram zamiast używać wywołania zwrotnego do powiadomienia wątku głównego.
Kotlin używa ramki stosu do zarządzania tym, która funkcja jest wykonywana, oraz wszystkimi zmiennymi lokalnymi. Podczas wstrzymywania współprogramu bieżąca ramka stosu jest kopiowana i zapisywana na później. Podczas wznawiania ramka stosu jest kopiowana z powrotem z miejsca, w którym została zapisana, a funkcja zaczyna działać ponownie. 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 współprogramów do zapewnienia bezpieczeństwa w wątku głównym
Współprogramy Kotlin używają dyspozytorów 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ć współprogramy Kotlin, aby wykonywały pracę w dyspozytorze Default lub IO. W Kotlinie wszystkie współprogramy muszą działać w dyspozytorze, nawet jeśli działają w wątku głównym. Współprogramy mogą się wstrzymywać, a dyspozytor jest odpowiedzialny za ich wznawianie.
Aby określić, gdzie mają działać współprogramy, Kotlin udostępnia 3 dyspozytory, których możesz używać:
- Dispatchers.Main – użyj tego dyspozytora, aby uruchomić współprogram w głównym
wątku Androida. Powinien być używany tylko do interakcji z interfejsem i wykonywania szybkich zadań. Przykłady obejmują wywoływanie
suspendfunkcji, wykonywanie operacji w ramach Android UI framework i aktualizowanieLiveDataobiektów. - 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. Przykłady obejmują używanie komponentu Room, odczytywanie i zapisywanie plików oraz wykonywanie operacji sieciowych.
- Dispatchers.Default – ten dyspozytor jest zoptymalizowany pod kątem wykonywania zadań wymagających dużej mocy obliczeniowej poza wątkiem głównym. Przykłady obejmują sortowanie listy i analizowanie kodu JSON.
Kontynuując poprzedni przykład, możesz użyć dyspozytorów, aby ponownie zdefiniować funkcję get. W treści funkcji get wywołaj withContext(Dispatchers.IO), aby utworzyć blok, który działa w puli wątków wejścia-wyjścia. Każdy kod umieszczony w tym bloku jest zawsze wykonywany za pomocą dyspozytora IO. Ponieważ withContext jest funkcją wstrzymującą, funkcja get jest również funkcją wstrzymują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
Dzięki współprogramom możesz wysyłać wątki z precyzyjną kontrolą. Ponieważ withContext() umożliwia kontrolowanie puli wątków dowolnej linii kodu bez wprowadzania wywołań zwrotnych, możesz zastosować ją do bardzo małych funkcji, takich jak odczytywanie z bazy danych lub wykonywanie żądania sieciowego. Dobrym rozwiązaniem jest używanie
withContext() w celu zapewnienia, że każda funkcja jest bezpieczna w wątku głównym, co oznacza, że możesz
wywołać ją z wątku głównego. W ten sposób element wywołujący 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 wątku głównym jest wznawiany z wynikiem get zaraz po zakończeniu bloku withContext.
Wydajność funkcji withContext()
withContext()
nie dodaje dodatkowego obciążenia w porównaniu z równoważną implementacją opartą na wywołaniach zwrotnych. Ponadto w niektórych sytuacjach można zoptymalizować wywołania withContext() poza równoważną implementacją opartą na wywołaniach zwrotnych. Jeśli na przykład funkcja wykonuje 10 wywołań sieciowych, możesz poinformować Kotlin, aby przełączał wątki tylko raz, używając zewnętrznej funkcji withContext(). Wtedy, nawet jeśli biblioteka sieciowa używa withContext() wiele razy, pozostaje w tym samym dyspozytorze i unika przełączania wątków. Ponadto 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
Współprogramy możesz uruchamiać na 2 sposoby:
launchuruchamia nowy współprogram i nie zwraca wyniku do elementu wywołującego. Każde zadanie, które jest uważane za „uruchom i zapomnij”, można uruchomić za pomocąlaunch.asyncuruchamia nowy współprogram i umożliwia zwrócenie wyniku za pomocą funkcji wstrzymującej o nazwieawait.
Zwykle należy launch nowy współprogram z normalnej funkcji, ponieważ normalna funkcja nie może wywołać await. Używaj async tylko w innym współprogramie lub w funkcji wstrzymującej i podczas wykonywania dekompozycji równoległej.
Dekompozycja równoległa
Wszystkie współprogramy uruchomione w funkcji suspend muszą zostać zatrzymane, gdy ta funkcja zwróci wartość, dlatego prawdopodobnie musisz zagwarantować, że te współprogramy zakończą się przed zwróceniem wartości. Dzięki strukturalnej współbieżności w Kotlinie możesz zdefiniować coroutineScope, który uruchamia co najmniej 1 współprogram. Następnie, używając await() (w przypadku pojedynczego współprogramu) lub awaitAll() (w przypadku wielu współprogramów), możesz zagwarantować, że te współprogramy zakończą się przed zwróceniem wartości z funkcji.
Na przykład zdefiniujmy coroutineScope, który asynchronicznie pobiera 2 dokumenty. Wywołując await() na każdym odroczonym odwołaniu, 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żyć 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 }
Chociaż fetchTwoDocs() uruchamia nowe współprogramy za pomocą async, funkcja używa awaitAll() do czekania na zakończenie tych uruchomionych współprogramów przed zwróceniem wartości. Pamiętaj jednak, że nawet jeśli nie wywołaliśmy awaitAll(), narzędzie do tworzenia coroutineScope nie wznawia współprogramu, który wywołał fetchTwoDocs, dopóki nie zakończą się wszystkie nowe współprogramy.
Ponadto coroutineScope przechwytuje wszystkie wyjątki zgłaszane przez współprogramy i przekazuje je z powrotem do elementu wywołującego.
Więcej informacji o dekompozycji równoległej znajdziesz w artykule Tworzenie funkcji wstrzymujących.
Pojęcia związane ze współprogramami
CoroutineScope
A CoroutineScope
śledzi wszystkie współprogramy, które tworzy za pomocą launch lub async. Trwające zadanie (czyli działające współprogramy) można anulować w dowolnym momencie, wywołując scope.cancel(). W Androidzie niektóre biblioteki KTX udostępniają własny 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 współprogramów.
viewModelScope jest też używany w przykładach w artykule
Wątki w tle na Androidzie za pomocą współprogramów.
Jeśli jednak musisz utworzyć własny CoroutineScope, aby kontrolować cykl życia współprogramów 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 współprogramów. Dlatego scope.cancel() należy wywoływać tylko wtedy, gdy klasa, która kontroluje jego cykl życia, jest niszczona. Gdy używasz viewModelScope, klasa
ViewModel automatycznie anuluje
zakres w metodzie onCleared() ViewModel.
Zadanie
Job
to uchwyt do współprogramu. Każdy współprogram utworzony 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 dodatkowo 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
A CoroutineContext
określa zachowanie współprogramu za pomocą tego zestawu elementów:
Job: kontroluje cykl życia współprogramu.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 CoroutineContext do funkcji launch lub async. Pamiętaj, że przekazanie Job do launch lub async nie ma żadnego efektu, ponieważ do nowego współprogramu 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 współprogramach
Więcej materiałów o współprogramach znajdziesz pod tymi linkami: