Zwiększanie wydajności aplikacji przy użyciu współprogramów Kotlin

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ów LiveData.
  • 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ą polecenia launch.
  • Narzędzie async uruchamia nową współpracę i pozwala zwrócić wynik za pomocą funkcji zawieszania o nazwie await.

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:

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: