Kotlin Kotlin umożliwiają pisanie czystego, uproszczonego kodu asynchronicznego, który zapewnia elastyczność aplikacji podczas zarządzania długotrwałymi zadaniami, takimi jak wywołania sieciowe czy operacje na dysku.
Ten artykuł zawiera szczegółowe informacje na temat współprogramów na Androidzie. Jeśli nie znasz się na aplikacjach, przeczytaj najpierw artykuł o kotlinach w języku Kotlin na Androida.
Zarządzanie długotrwałymi zadaniami
Korutyny wykorzystują zwykłe funkcje, dodając 2 operacje do obsługi długotrwałych zadań. Oprócz poleceń invoke
(lub call
) i return
współprogramy dodają suspend
i resume
:
suspend
wstrzymuje wykonanie bieżącej współpracy, zapisując wszystkie zmienne lokalne.resume
kontynuuje wykonywanie zawieszonej synchronizacji od miejsca, w którym została zawieszona.
Funkcje suspend
możesz wywoływać tylko z innych funkcji suspend
lub za pomocą konstruktora współprogramów, takiego jak launch
, w celu uruchomienia nowej współpracy.
Poniżej znajduje się przykład prostej implementacji kodu 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 wątku głównym, ale zawiesza Korutę przed wysłaniem żądania sieciowego. Po zakończeniu żądania sieciowego get
wznawia zawieszoną współpracę, zamiast korzystać z wywołania zwrotnego do powiadamiania wątku głównego.
Kotlin używa ramki stosu do zarządzania działaniem funkcji wraz ze zmiennymi lokalnymi. Podczas zawieszania współprogramowania bieżąca ramka stosu jest kopiowana i zapisywana na później. Podczas wznawiania ramka stosu jest kopiowana z miejsca, w którym została zapisana, i funkcja zaczyna działać ponownie. Mimo że kod może wyglądać jak zwykłe żądanie blokowania sekwencyjnego, współpraca dba o to, aby żądanie sieciowe nie blokujeło wątku głównego.
Używanie współprogramów do zapewniania bezpieczeństwa
Kortyny Kotlin używają dyspozytorów do określania, które wątki są używane do wykonywania współużytkowania. Aby uruchomić kod poza wątkiem głównym, możesz poinstruować współtwórcy Kotlin, aby wykonywał pracę na dyspozytora domyślnego lub IO. W Kotlin wszystkie współprace muszą działać w dyspozytorze, nawet jeśli działają w wątku głównym. Korutyny mogą zawiesić się samodzielnie, a dyspozytor odpowiada za ich wznowienie.
Aby określić, gdzie powinny działać współprogramy, Kotlin udostępnia 3 dyspozytorów, których możesz używać:
- Dispatchers.Main (Dispatchers.Main) – użyj tego dyspozytora, by uruchomić współpracę w głównym wątku Androida. Należy go używać tylko do interakcji z interfejsem i szybkiej pracy. Może to być na przykład wywoływanie funkcji
suspend
, uruchamianie operacji platformy interfejsu Androida i aktualizowanie obiektówLiveData
. - Dispatchers.IO – ten dyspozytor jest zoptymalizowany pod kątem wykonywania operacji wejścia-wyjścia dysku lub sieci poza wątek główny. Korzystanie z komponentu Pokój, odczytywanie plików i zapisywanie ich oraz uruchamianie dowolnych operacji sieciowych.
- Dispatchers.Default (Dispatchers.Default) – ten dyspozytor jest zoptymalizowany do wykonywania zadań wymagających dużej mocy obliczeniowej poza wątkiem głównym. Przykładowe zastosowania obejmują sortowanie listy i analizowanie danych w formacie JSON.
Nawiązując do poprzedniego przykładu, możesz skorzystać z dyspozytorów, aby ponownie zdefiniować funkcję get
. W treści get
wywołaj withContext(Dispatchers.IO)
, aby utworzyć blok uruchamiany w puli wątków zamówień reklamowych. Każdy kod umieszczony w tym bloku jest zawsze uruchamiany przez dyspozytora IO
. Ponieważ withContext
sam w sobie jest funkcją zawieszania, funkcja get
jest również funkcją zawieszania.
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 ze szczegółową kontrolą. Ponieważ właściwość withContext()
umożliwia kontrolowanie puli wątków dowolnego wiersza kodu bez wprowadzania wywołań zwrotnych, można go stosować do bardzo małych funkcji, takich jak odczyt z bazy danych lub wykonywanie żądań sieciowych. Sprawdzoną metodą jest użycie właściwości withContext()
, aby każda funkcja była bezpieczna dla głównej, co oznacza, że można ją wywoływać z wątku głównego. Dzięki temu osoba wywołująca nie musi się zastanawiać, którego wątku użyć do wykonania funkcji.
W poprzednim przykładzie funkcja fetchDocs()
jest wykonywana w wątku głównym, ale może bezpiecznie wywołać usługę get
, która wykonuje żądanie sieciowe w tle.
Ponieważ współprogramy obsługują suspend
i resume
, współpraca w wątku głównym jest wznawiana z wynikiem get
zaraz po zakończeniu blokady withContext
.
Wydajność funkcji withContext()
withContext()
nie zwiększa nakładu pracy w porównaniu z odpowiednią implementacją opartą na wywołaniu zwrotnym. Ponadto w niektórych sytuacjach można optymalizować wywołania withContext()
poza równoważną implementacją opartą na wywołaniach zwrotnych. Jeśli na przykład funkcja wysyła 10 wywołań do sieci, możesz sprawić, że Kotlin przełączy wątki tylko raz, używając zewnętrznego obiektu withContext()
. Następnie, mimo że biblioteka sieciowa wielokrotnie używa metody withContext()
, pozostaje ona w ramach tego samego dyspozytora i nie przełącza się wątków. Dodatkowo Kotlin optymalizuje przełączanie się między Dispatchers.Default
a Dispatchers.IO
, aby w miarę możliwości uniknąć przełączania wątków.
Uruchom współprogram
współprogramy można uruchomić na 2 sposoby:
launch
uruchamia nową współprogram i nie zwraca wyniku do elementu wywołującego. Każdą pracę z oznaczeniem „wypal i zapomnij” można rozpocząć za pomocą polecenialaunch
.- Narzędzie
async
uruchamia nową współpracę i pozwala zwrócić wynik za pomocą funkcji zawieszania o nazwieawait
.
Zwykle należy wykonać launch
nową współpracę z funkcji zwykłej, ponieważ zwykła funkcja nie może wywołać funkcji await
. async
należy używać tylko w innej współrzędnej lub w ramach funkcji zawieszania i wykonywaniu rozkładu równoległego.
Rozłożenie równoległe
Wszystkie współprogramy uruchamiane w funkcji suspend
muszą być zatrzymywane po jej zwróceniu, więc prawdopodobnie musisz zagwarantować, że te współprogramy zakończą się przed zwróceniem. Za pomocą uporządkowanej równoczesności w Kotlin możesz zdefiniować element coroutineScope
, który będzie uruchamiać co najmniej 1 współpracę. Następnie używając await()
(w przypadku pojedynczej współprogramy) lub awaitAll()
(w przypadku wielu współprogramów), możesz zagwarantować, że te współprogramy zostaną ukończone przed zwróceniem z funkcji.
Przykład: zdefiniujmy obiekt coroutineScope
, który asynchronicznie będzie pobierać 2 dokumenty. Wywołując await()
przy 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()
}
Funkcji awaitAll()
możesz też używać w kolekcjach, tak jak 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
}
fetchTwoDocs()
uruchamia nowe współprogramy z użyciem async
, jednak funkcja używa awaitAll()
, aby przed zwróceniem czekać na zakończenie tych uruchomionych współprogramów. Pamiętaj jednak, że nawet jeśli nie wywołaliśmy funkcji awaitAll()
, kreator coroutineScope
nie wznowi współpracy, która wywołała fetchTwoDocs
, dopóki wszystkie nowe współprogramy nie zostaną ukończone.
Dodatkowo coroutineScope
wykrywa wszystkie wyjątki zgłaszane przez współprogramy i przekierowuje je z powrotem do elementu wywołującego.
Więcej informacji o równoległym rozkładaniu znajdziesz w artykule o tworzeniu funkcji zawieszania.
Pojęcia związane z koretynami
Zakres Coroutine
CoroutineScope
śledzi każdą współpracę, którą tworzy za pomocą launch
lub async
. Ciągłą pracę (tj. działającą) można w każdej chwili anulować, wywołując metodę scope.cancel()
. Na Androidzie niektóre biblioteki KTX udostępniają własne CoroutineScope
dla określonych klas cyklu życia. Na przykład ViewModel
zawiera viewModelScope
, a Lifecycle
zawiera lifecycleScope
.
W przeciwieństwie do dyspozytora CoroutineScope
nie obsługuje jednak współprogramów.
Funkcja viewModelScope
jest też używana w przykładach
w tle na Androidzie z koderami.
Jeśli jednak chcesz utworzyć własny CoroutineScope
, aby kontrolować cykl życia współprogramów w konkretnej 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 utworzyć więcej współprogramów. Dlatego wywoływaj 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()
obiektu ViewModel.
Zadanie
Job
to nick współrzędu. Każda współpraca utworzona za pomocą funkcji launch
lub async
zwraca instancję Job
, która jednoznacznie identyfikuje ją i zarządza jej cyklem życia. Możesz też przekazać Job
do CoroutineScope
, aby zarządzać cyklem życia tego produktu, jak w tym przykładzie:
class ExampleClass {
...
fun exampleMethod() {
// Handle to the coroutine, you can control its lifecycle
val job = scope.launch {
// New coroutine
}
if (...) {
// Cancel the coroutine started above, this doesn't affect the scope
// this coroutine was launched in
job.cancel()
}
}
}
Kontekst Coroutine
CoroutineContext
definiuje działanie współpracy za pomocą tego zestawu elementów:
Job
: Steruje cyklem życia współprogramy.CoroutineDispatcher
: wysyłanie działają w odpowiednim wątku.CoroutineName
: nazwa wtyczki przydatna do debugowania.CoroutineExceptionHandler
: obsługuje niewykryte wyjątki.
W przypadku nowych współprac w obrębie zakresu do nowej współużytkowanej zostaje przypisana nowa instancja Job
, a pozostałe elementy CoroutineContext
są dziedziczone z zakresu, który zawiera. Możesz zastąpić odziedziczone elementy, przekazując nowy element CoroutineContext
do funkcji launch
lub async
. Zwróć uwagę, że przekazanie interfejsu Job
do launch
ani async
nie przynosi żadnych skutków, ponieważ nowe wystąpienie Job
jest zawsze przypisywane do nowej współpracy.
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 zasoby współprogramów
Więcej zasobów dotyczących współprogramów znajdziesz pod tymi linkami: