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

Współrzędne partnerskie Kotlin pozwalają napisać czysty, uproszczony kod asynchroniczny, który zachowuje responsywność aplikacji przy zarządzaniu długotrwałymi zadaniami, takimi jak wywołania sieciowe. lub operacje na dysku.

W tym artykule znajdziesz szczegółowe omówienie współrzędnych na Androidzie. Jeśli nie znasz współrzędnych, koniecznie przeczytaj Zanim przeczytasz ten temat, Kotlin współpracuje na Androidzie.

Zarządzaj długotrwałymi zadaniami

Kogutyny bazują na zwykłych funkcjach przez dodanie 2 operacji do obsługi do długotrwałych zadań. Oprócz invoke (lub call) i return, współprogramy dodają suspend i resume:

  • suspend wstrzymuje wykonanie bieżącej współrzędnej, zapisując wszystkie zmiennych.
  • resume kontynuuje wykonywanie zawieszonej współprogramu w miejscu na którym zostały zawieszone.

Funkcje suspend możesz wywoływać tylko z poziomu innych funkcji suspend lub za pomocą kreatora współoutin, takiego jak launch, aby uruchomić nową współrzędną.

Poniższy przykład pokazuje prostą implementację współrzędną dla hipotetyczne zadanie długotrwałe:

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 reguła get() nadal działa w wątku głównym, ale zawiesza przed rozpoczęciem żądania sieciowego. Gdy żądanie sieciowe zakończy, get wznawia zawieszoną współpracę zamiast używać wywołania zwrotnego , aby powiadomić wątek główny.

Kotlin wykorzystuje ramkę stosu do określania, która funkcja działa z dowolnymi zmiennymi lokalnymi. Przy zawieszaniu współprogramu bieżący stos jest skopiowana i zapisana na później. Podczas wznawiania ramka stosu ma postać zostanie skopiowany z powrotem z miejsca, w którym została zapisana, a funkcja zostanie ponownie uruchomiona. Mimo że kod może wyglądać jak zwykłe, sekwencyjne blokowanie żądania sieciowe, zapewnia, że żądanie sieciowe nie będzie blokować w wątku głównym.

Używaj wspólnych reguł na potrzeby głównego bezpieczeństwa

W Kotlinach używane są dyspozytory określające, które wątki są używane wspólnego wykonania. Aby uruchomić kod poza wątkiem głównym, możesz poinformować Kotlin i wykonują pracę nad dyspozytorem domyślnym lub IO. W Kotlin, wszystkie współudziały muszą być uruchomione w dyspozytorze, nawet jeśli są uruchamiane w wątku głównym. Koutyny mogą zawiesić się, a dyspozytor i odpowiada za ich wznowienie.

Aby określić, gdzie powinny być uruchamiane współudziały, Kotlin udostępnia 3 dyspozytorów których możesz użyć:

  • Dispatchers.Main – użyj tego dyspozytora, by uruchomić współprogram w głównym Wątek dotyczący Androida. Należy go używać wyłącznie do interakcji z interfejsem użytkownika szybkiej pracy. Przykłady obejmują wywoływanie funkcji suspend, uruchamianie operacje i aktualizacje platformy interfejsu Androida; LiveData obiektów.
  • Dispatchers.IO – ten dyspozytor jest zoptymalizowany do wykonywania zadań na dyskach lub w sieci Wejście-wyjście poza wątkiem głównym. Przykłady: komponent Sala, do odczytywania plików i zapisywania do nich oraz wykonywania operacji sieciowych.
  • Dispatchers.Default – ten dyspozytor jest zoptymalizowany pod kątem Obciążenie procesora przez pracę poza wątkiem głównym. Przykładowe zastosowania to sortowanie i przeanalizować kod JSON.

Kontynuując poprzedni przykład, możesz użyć dyspozytorów, aby ponownie zdefiniować get. Wewnątrz treści get wywołaj withContext(Dispatchers.IO) do utworzyć blok, który działa w puli wątków we/wy. każdy umieszczony w nim kod, jest zawsze wykonywany przez dyspozytora IO. Ponieważ withContext jest sam w sobie funkcji zawieszenia (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ż withContext() pozwala kontrolować pulę wątków dowolnego wiersza kodu bez konieczności wprowadzając wywołania zwrotne, można je zastosować do bardzo małych funkcji, takich jak odczytywanie z bazy danych lub wykonując żądania sieciowe. Zalecaną praktyką jest withContext(), aby zapewnić, że każda funkcja jest bezpieczna, co oznacza może wywołać funkcję z wątku głównego. Dzięki temu rozmówca nigdy nie będzie musiał i pomyśl, którego wątku należy użyć do wykonania funkcji.

W poprzednim przykładzie polecenie fetchDocs() jest wykonywane w wątku głównym; jednak może bezpiecznie wywołać get, który wykonuje żądanie sieciowe w tle. Ponieważ współprogramy obsługują suspend i resume, współrzędna w głównym wątek zostanie wznowiony z wynikiem get, gdy tylko blok withContext zostanie gotowe.

Wydajność withContext()

withContext() nie stanowi dodatkowego narzutu w porównaniu z równoważnym rozwiązaniem opartym na wywołaniu zwrotnym implementacji. Ponadto można zoptymalizować wywołania funkcji withContext() w pewnych sytuacjach wykraczające poza równoważną implementację opartą na wywołaniach zwrotnych. Dla: na przykład jeśli funkcja wysyła dziesięć wywołań sieci, możesz użyć polecenia Kotlin przełączać wątki tylko raz przy użyciu zewnętrznego elementu withContext(). Potem, mimo że biblioteka sieciowa używa funkcji withContext() wielokrotnie, pozostaje bez zmian z dyspozytorem i unikać przełączania wątków. Kotlin optymalizuje też przechodzenie między Dispatchers.Default a Dispatchers.IO, by uniknąć przełączania wątków gdy tylko jest to możliwe.

Rozpoczynanie współpracy

Współrzędne można rozpoczynać na 2 sposoby:

  • launch rozpoczyna nową współudział i nie zwraca wyniku wywołującemu. Dowolne utwór uważany za „ogień i zapomnienie” można rozpocząć za pomocą funkcji launch.
  • async uruchamia nową współpracę i umożliwia zwrócenie wyniku ze stanem zawieszenia funkcja o nazwie await.

Zwykle należy launch nową współudział ze zwykłej funkcji, jako zwykła funkcja nie może wywołać funkcji await. Używaj async tylko w środku w innej koryturze lub w ramach funkcji zawieszenia rozkładu równoległego.

Rozkład równoległy

Wszystkie współprogramy uruchomione w funkcji suspend muszą zostać zatrzymane, gdy ta funkcja się zwraca, więc prawdopodobnie musisz zagwarantować, że te współrzędne zanim ją zwrócisz. Dzięki ustrukturyzowanej równoczesności w Kotlin możesz określić, coroutineScope, który uruchamia co najmniej 1 współpracę. Następnie za pomocą funkcji await() (w przypadku pojedynczej korygu) lub awaitAll() (w przypadku wielu współprogramów), możesz zagwarantuje zakończenie tego procesu przed powrótem z funkcji.

Przykładowo zdefiniujmy coroutineScope, który pobiera 2 dokumenty asynchronicznie. Wywołując await() przy każdym odroczonym pliku referencyjnym, 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()
    }

Elementu awaitAll() możesz też użyć w kolekcjach, 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
    }

Mimo że fetchTwoDocs() uruchamia nowe współrzędne z async, funkcja używa funkcji awaitAll() do oczekiwania na zakończenie uruchamiania współprac przez te, które zostały uruchomione przed powrót do domu. Pamiętaj jednak, że nawet gdybyśmy nie wywołali awaitAll(), Kreator coroutineScope nie wznawia współprogramu, który wywołał fetchTwoDocs do zakończenia wszystkich nowych współprogramów.

Dodatkowo coroutineScope wykrywa wszystkie wyjątki zgłaszane przez współrzędne i przekierowuje go z powrotem do rozmówcy.

Więcej informacji o rozkładzie równoległym znajdziesz w artykule Tworzenie funkcji zawieszania

Pojęcia związane z koutynami

CoroutineScope

CoroutineScope śledzi wszystkie tworzone współprogramy za pomocą elementów launch lub async. trwające prace (tj. aktywne współprace) można anulować, wywołując scope.cancel(). Na Androidzie niektóre biblioteki KTX udostępniają własne CoroutineScope dla określonych klas cyklu życia. Przykład: ViewModel ma viewModelScope, a Lifecycle ma lifecycleScope. Jednak w przeciwieństwie do dyspozytora CoroutineScope nie uruchamia współrzędnych.

viewModelScope jest też używany w przykładach w: Wątki w tle na urządzeniach z Androidem za pomocą Coroutines. Jeśli jednak musisz utworzyć własny element CoroutineScope, aby kontrolować cyklu życia współprogramów w określonej warstwie aplikacji, możesz utworzyć w następujący 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ółrzędnych. Dlatego Wywołuj funkcję scope.cancel() tylko wtedy, gdy klasa kontroluje cykl życia jest zniszczona. W przypadku użycia funkcji viewModelScope makro Zajęcia ViewModel anulują zakresu w metodzie onCleared() modelu ViewModel.

Zadanie

Job jest nickiem dla współprogramu. Każda współrzędna utworzona za pomocą funkcji launch lub async zwraca instancję Job, która jednoznacznie identyfikuje współdziała z internetem i zarządza jego cyklem życia. Możesz również przekazać Job do CoroutineScope, aby dokładniej zarządzać cyklem życia, jak pokazano poniżej przykład:

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 zachowanie współrzędnej przy użyciu takiego zestawu elementów:

W przypadku nowych współprogramów utworzonych w zakresie nowa instancja Job jest przypisane do nowej współrzędnej, a pozostałe elementy CoroutineContext są dziedziczone z zakresu, który zawiera. Możesz zastąpić odziedziczone elementów, przekazując nowy CoroutineContext do launch lub async . Pamiętaj, że przekazanie wartości Job do launch lub async nie ma żadnego efektu. jako nowa instancja instancji Job jest zawsze przypisywana do nowej kompatybilności.

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

Aby uzyskać więcej zasobów o współudziałach, kliknij te linki: