Większa skuteczność dzięki podziale na wątki

Właściwe korzystanie z wątków na Androidzie może poprawić wyniki aplikacji skuteczność reklam. Na tej stronie omawiamy kilka aspektów pracy z wątkami: praca z interfejsem lub głównym wątkiem; zależności między cyklem życia aplikacji priorytet wątku; oraz o metodach zapewnianych przez platformę ułatwiających zarządzanie wątkami i jego złożoność. W każdym z tych obszarów znajdziesz na tej stronie potencjalne pułapki oraz strategii ich unikania.

Wątek główny

Gdy użytkownik uruchamia Twoją aplikację, Android tworzy nowy system Linux wraz z wątkiem wykonania. W tym wątku głównym zwanym wątkiem UI, odpowiada za wszystko, co się dzieje na ekranie. Wiedza o tym, jak to działa, może pomóc w zaprojektowaniu aplikacji pod kątem korzystania w wątku głównym, aby uzyskać jak najlepszą wydajność.

Wewnętrzne

Główny wątek ma bardzo prosty projekt: jedynym zadaniem jest bloki pracy z kolejki roboczej zapewniającej bezpieczeństwo w wątkach do momentu zamknięcia aplikacji. platformy generują niektóre z tych bloków pracy z różnych miejsc. Te Miejsca obejmują wywołania zwrotne związane z informacjami o cyklu życia, zdarzenia użytkownika takie jak jako dane wejściowe lub zdarzenia pochodzące z innych aplikacji i procesów. Ponadto aplikacja może jawnego umieszczania bloków w kolejce, bez korzystania z platformy.

Prawie dowolne blok kodu wykonywany przez aplikację jest powiązany z wywołaniem zwrotnym zdarzenia, np. z danymi wejściowymi, inflację układu, czyli rysowanie. Gdy coś wywoła zdarzenie, wątek, w którym to zdarzenie wypchnięto zdarzenie z samego siebie i do wiadomości w wątku głównym kolejkę. Wątek główny może obsługiwać wydarzenie.

W trakcie animacji lub aktualizacji ekranu system próbuje wykonać blok pracy (który odpowiada za rysowanie ekranu) co około 16 ms, aby płynnie renderować się przy 60 klatek na sekundę. Aby system mógł osiągnąć ten cel, hierarchia UI/widoku musi zostać zaktualizowana w wątku głównym. Gdy jednak kolejka wiadomości w wątku głównym zawiera zadania, które są zbyt liczne lub zbyt długie, aby zmieścić się w wątku głównym aktualizacja dostatecznie szybko, aplikacja powinna przenieść tę pracę do instancji roboczej w wątku. Jeśli wątek główny nie może zakończyć wykonywania bloków pracy w ciągu 16 ms, użytkownik może zauważyć przerwy, opóźnienia lub brak responsywności interfejsu użytkownika. Jeśli wątek główny blokuje się przez około 5 sekund, system wyświetla Aplikacja Okno ANR (Nie odpowiada), które umożliwia użytkownikowi bezpośrednie zamknięcie aplikacji.

przenoszenie wielu lub długich zadań z wątku głównego, tak aby nie zakłócały one pracy; płynne renderowanie i szybkie reagowanie na dane wejściowe użytkownika. jest powód do wdrożenia w aplikacji podziału na wątki.

Odwołania do wątków i obiektów interfejsu

Z założenia Android Obiekty widoku nie są bezpieczne w wątkach. Aplikacja ma tworzyć, używać niszczenia obiektów UI, a wszystko to w wątku głównym. Jeśli spróbujesz zmienić a nawet odwołanie do obiektu UI w wątku innym niż główny, wynik mogą obejmować wyjątki, dyskretne awarie, awarie i inne nieokreślone błędy.

Problemy z plikami referencyjnymi dzielą się na 2 różne kategorie: bezpośrednie odniesienia i niejawne odwołania.

Bezpośrednie odwołania

Wiele zadań w wątkach innych niż główne ma na celu aktualizowanie obiektów UI. Jeśli jednak jeden z tych wątków uzyska dostęp do obiektu w hierarchii widoków, może spowodować niestabilność aplikacji: jeśli wątek instancji roboczej zmieni właściwości w tym samym czasie, w którym odwołuje się do niego każdy inny wątek, wyniki są niezdefiniowane.

Rozważmy na przykład aplikację, która zawiera bezpośrednie odniesienie do obiektu UI w wątku instancji roboczej. Obiekt w wątku instancji roboczej może zawierać odniesienie do View; ale przed zakończeniem pracy View jest z hierarchii widoków. Jeśli te 2 działania zachodzą jednocześnie, odwołanie zachowuje obiekt View w pamięci i ustawia w nim właściwości. Jednak użytkownik nigdy nie zobaczy, ten obiekt, a aplikacja usunie go, gdy odwołanie do niego zniknie.

W innym przykładzie obiekty View zawierają odwołania do działania. ich właścicielem. Jeśli że aktywność zostanie zniszczona, ale pozostanie blok z wątkiem, odwołuje się do niej – bezpośrednio lub pośrednio – moduł czyszczenia pamięci nie będzie aż do zakończenia wykonywania tego bloku pracy.

Ten scenariusz może powodować problem w sytuacjach, w których praca z wątkami może być okres wyświetlania, gdy wystąpi pewne zdarzenie cyklu życia działania, takie jak obrót ekranu. System nie byłby w stanie przeprowadzić procesu czyszczenia pamięci, dopóki nie będzie które udało się ukończyć. W wyniku tego w argumencie Activity mogą być 2 obiekty pamięci, dopóki nie rozpocznie się proces czyszczenia.

W takich sytuacjach aplikacja nie powinna zawierać treści dla dorosłych odwołania do obiektów UI w zadaniach roboczych w wątkach. Unikanie takich odwołań pozwala uniknąć tego rodzaju wyciek pamięci, a jednocześnie unikał rywalizacji o wątki.

We wszystkich przypadkach aplikacja powinna aktualizować tylko obiekty interfejsu w wątku głównym. Ten Oznacza to, że należy stworzyć zasady negocjacji, które dopuszczają przekazać pracę z wątkiem głównym, którym zajmuje się najbardziej z aktualizacją rzeczywistego obiektu UI.

Odwołania ogólne

Powszechną usterkę w projekcie kodu związaną z obiektami z wątkami można zauważyć we fragmencie kodu poniżej:

Kotlin

class MainActivity : Activity() {
    // ...
    inner class MyAsyncTask : AsyncTask<Unit, Unit, String>() {
        override fun doInBackground(vararg params: Unit): String {...}
        override fun onPostExecute(result: String) {...}
    }
}

Java

public class MainActivity extends Activity {
  // ...
  public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
    @Override protected String doInBackground(Void... params) {...}
    @Override protected void onPostExecute(String result) {...}
  }
}

Błąd w tym fragmencie polega na tym, że w kodzie deklaruje obiekt threading MyAsyncTask jako niestatyczna klasa wewnętrzna pewnej aktywności (lub klasa wewnętrzna) w Kotlin). Ta deklaracja tworzy niejawne odwołanie do zamykającego elementu Activity instancji. W rezultacie obiekt zawiera odniesienie do działania do momentu praca z wątkami zostaje zakończona, co powoduje opóźnienie w zniszczeniu wskazanego działania. To z kolei wywiera większy nacisk na pamięć.

Bezpośrednim rozwiązaniem tego problemu jest zdefiniowanie przeciążonej klasy instancji jako klas statycznych lub w ich własnych plikach, co powoduje usunięcie odnośnik niejawny.

Innym rozwiązaniem jest anulowanie i czyszczenie zadań w tle w odpowiednim Wywołanie zwrotne cyklu życia Activity, np. onDestroy. Takie podejście może żmudne i podatne na błędy. Zasadniczo nie należy używać złożonej logiki nieobsługującej interfejsu użytkownika bezpośrednio w aktywnościach. Oprócz tego interfejs AsyncTask został wycofany i jest nie jest zalecane do użycia w nowym kodzie. Zobacz Threading na Androidzie. .

Cykle życia wątków i aktywności w aplikacjach

Cykl życia aplikacji może mieć wpływ na działanie wątków w aplikacji. Możesz zdecydować, czy wątek ma pozostawać czy nie powinien pozostawać po Musisz również zdawać sobie sprawę z relacji między określanie priorytetów wątków oraz określanie, czy aktywność jest uruchomiona na pierwszym planie, w tle.

Trwałe wątki

Wątki pozostają w życiu przez wszystkie działania, które je wywołały. Wątki są nadal wykonywane, nieprzerwanie niezależnie od tego, czy zostały utworzone czy zniszczone, choć zostaną one zakończone wraz z procedurą zgłoszenia, gdy nie będzie bardziej aktywne komponenty aplikacji. W niektórych przypadkach jest to korzystne.

Rozważmy przypadek, w którym aktywność powoduje powstawanie zestawu bloków roboczych z wątkami. jest następnie zniszczony, zanim wątek roboczy będzie mógł uruchomić bloki. Do czego służą co robimy z unoszącymi się w powietrzu blokami?

Jeśli blokady miałyby aktualizować interfejs, który już nie istnieje, nie ma powodu. aby kontynuował pracę. Jeśli na przykład ma to na celu wczytanie informacji o użytkownikach z bazy danych, a następnie aktualizować widoki, wątek nie jest już potrzebny.

Z kolei pakiety służbowe mogą przynosić pewne korzyści niezwiązane z Interfejs. W takim przypadku należy utrwalić wątek. Pakiety mogą być na przykład oczekiwanie na pobranie obrazu, zapisanie go w pamięci podręcznej i zaktualizowanie powiązanych View obiekt. Chociaż obiekt już nie istnieje, podczas pobierania zapisanie obrazu w pamięci podręcznej może być pomocne, na wypadek gdyby użytkownik wrócił do a niszczycielstwo.

Ręczne zarządzanie odpowiedziami cyklu życia wszystkich obiektów z wątkami może stać się bardzo złożonym procesem. Jeśli nie będziesz nimi zarządzać prawidłowo, w przypadku aplikacji mogą wystąpić rywalizacja o pamięć i z wydajnością. Łączę ViewModel wraz z LiveData umożliwia: wczytuj dane i otrzymuj powiadomienia, gdy się zmienią bez obaw o cykl życia. ViewModel obiektów jest rozwiązania tego problemu. Modele widoków są utrzymywane po zmianach konfiguracji, które zapewnia łatwy sposób na zachowanie danych widoku. Więcej informacji o modelach widoków znajdziesz w dokumentacji Przewodnik po ViewModel i więcej informacji na jego temat LiveData zapoznaj się z przewodnikiem LiveData. Jeśli Użytkownik chciałby też dowiedzieć się więcej o architekturze aplikacji, Przewodnik po architekturze aplikacji.

Priorytet wątku

Jak opisano w sekcji Procesy i Cykl życia aplikacji, czyli priorytet, jaki otrzymują wątki aplikacji. zależy częściowo od tego, na jakim etapie cyklu życia aplikacji znajduje się aplikacja. Podczas tworzenia do zarządzania wątkami w aplikacji, ważne jest, aby określić ich priorytety, że odpowiednie wątki uzyskują odpowiednie priorytety we właściwym czasie. Jeśli ustawisz zbyt wysoką wartość, może on przerwać wątek UI i RenderThread, przez co aplikacja i usuwać klatki. Jeśli ustawienie będzie za niskie, możesz utworzyć zadania asynchroniczne (np. graficzne ładowanie) wolniej, niż jest to konieczne.

Za każdym razem, gdy tworzysz wątek, musisz wywołać setThreadPriority() Wątek systemowy algorytm szeregowania preferuje wątki o wysokim priorytecie, równoważąc te i traktujemy priorytetowo, aby ostatecznie zakończyć całą pracę. Ogólnie rzecz biorąc, na pierwszym planie grupy otrzymują około 95% łącznego czasu wykonywania na urządzeniu, grupa tła ma około 5%.

System przypisuje każdemu wątkowi własną wartość priorytetu, korzystając z metody Process zajęcia.

Domyślnie system ustawia priorytet wątku na ten sam priorytet i tę samą grupę jako pierwszy wątek. Aplikacja może jednak wyraźnie dostosuj priorytet wątku za pomocą setThreadPriority()

Process pomaga uprościć przypisywanie wartości priorytetów, zapewniając zestaw stałych, których aplikacja może używać do określania priorytetów wątków. Przykład: THREAD_PRIORITY_DEFAULT reprezentuje domyślną wartość wątku. Aplikacja powinna ustawić priorytet wątku na THREAD_PRIORITY_BACKGROUND dla wątków wykonujących mniej pilne zadania.

Twoja aplikacja może używać tych uprawnień: THREAD_PRIORITY_LESS_FAVORABLE i THREAD_PRIORITY_MORE_FAVORABLE stałe jako przyrosty, aby ustawić względne priorytety. Lista: priorytety wątków, patrz THREAD_PRIORITY stałych w klasy Process.

Więcej informacji na temat: zarządzania wątkami, zapoznaj się z dokumentacją na temat Thread i Process zajęć.

Klasy pomocnicze do podziału na wątki

W przypadku deweloperów, których językiem głównym jest Kotlin, zalecamy korzystanie z współrzędnych. Kogutyny mają wiele zalet, w tym możliwość pisania kodu asynchronicznego bez wywołań zwrotnych, a także uporządkowanej równoczesności do określania zakresu, anulowania i obsługi błędów.

Platforma udostępnia również te same klasy i elementy podstawowe w Javie, co ułatwia wątki, takie jak Thread, Runnable i Executors zajęć, oraz dodatkowe, takie jak HandlerThread. Więcej informacji znajdziesz w artykule o Threading na Androidzie.

Klasa HandlerThread

Wątek modułu obsługi to długotrwały wątek, który pobiera zadania z kolejki i działa. .

Pomyśl o typowym problemie z uzyskaniem klatek podglądu z tagów Camera obiekt. Gdy zarejestrujesz się do korzystania z klatek podglądu aparatu, otrzymasz je w onPreviewFrame() wywołanie zwrotne w wątku zdarzenia, z którego zostało wywołane. Jeśli które było wywołanie zwrotne w wątku UI. Zadanie polega na obsłudze ogromnego kolidowałyby z renderowaniem i przetwarzaniem zdarzeń.

W tym przykładzie, gdy aplikacja przekaże polecenie Camera.open() do blok pracy nad wątkiem modułu obsługi, powiązany blok onPreviewFrame() oddzwanianie trafia do wątku modułu obsługi, a nie wątku UI. Jeśli więc planujesz długofalowo nad pikselami, może to być lepsze rozwiązanie.

Gdy Twoja aplikacja utworzy wątek przy użyciu funkcji HandlerThread, nie zapomnij o ustawieniu na podstawie typu wykonywanej pracy. Pamiętaj, że procesory mogą obsługuje równoległą liczbę wątków. Pomaga określenie priorytetu system zna właściwe sposoby planowania tej pracy, gdy wszystkie inne wątki walczą o uwagę.

Klasa ThreadPoolExecutor

Istnieją pewne rodzaje pracy, które można ograniczyć do poziomu wysoce równoległego, rozproszonych zadań. Do takich zadań należy na przykład obliczanie filtra dla każdego Blok o wymiarach 8 x 8 obrazu w rozdzielczości 8 megapikseli. Ze względu na dużą liczbę pakietów służbowych klasa HandlerThread nie jest odpowiednią klasą.

ThreadPoolExecutor to klasa pomocnicza, ten proces. Ta klasa zarządza tworzeniem grupy wątków, zbiorów ich priorytety i zarządzanie sposobem podziału pracy między te wątki. W miarę zwiększania lub zmniejszania zbioru zadań klasa uruchamia lub niszczy więcej wątków aby dostosować się do zadania.

Ta klasa pomaga również aplikacji uzyskać optymalną liczbę wątków. Kiedy to tworzy ThreadPoolExecutor obiektu, aplikacja ustawia minimalne i maksymalne wartości liczby wątków. Ponieważ zbiór zadań przypisany do ThreadPoolExecutor rośnie, klasa weźmie zainicjowaną minimalną i maksymalną liczbę wątków konta i zastanów się, ile zostało do zrobienia. Na podstawie tych ThreadPoolExecutor określa, ile i wątki powinny być aktywne w każdej chwili.

Ile wątków trzeba utworzyć?

Mimo że na poziomie oprogramowania kod może tworzyć setki wątków, co może powodować problemy z wydajnością. Aplikacja ma ograniczony udział CPU z usługami w tle, mechanizmem renderowania, mechanizmem audio i nie tylko. Procesory mają tak naprawdę możliwość równoległej obsługi niewielkiej liczby wątków; wszystko powyżej w kwestii priorytetu i harmonogramu. Dlatego ważne jest, aby tworzyć tylko tyle wątków, ile potrzebuje Twój zbiór zadań.

Zasadniczo jest za to odpowiedzialny szereg zmiennych, wyboru wartości (np. 4, czyli wartości inicjującej) i testowania jej Systrace to solidną strategię jak każda inna. Za pomocą metody prób i błędów możesz odkryć czyli minimalną liczbę wątków, których można użyć bez problemów.

Innym problemem przy podejmowaniu decyzji o liczbie wątków jest to, nie są bezpłatne: zajmują pamięć. Każdy wątek kosztuje co najmniej 64 tys. pamięci. Dane te szybko trafiają do wielu aplikacji zainstalowanych na urządzeniu, szczególnie w sytuacjach, w których stos wywołań znacznie się rozwija.

Wiele procesów systemowych i bibliotek zewnętrznych często tworzy własne pule wątków. Jeśli aplikacja może ponownie użyć istniejącej puli wątków, to ponowne wykorzystanie może pomóc wydajności przez zmniejszanie rywalizacji o zasoby pamięci i przetwarzania.