Powolne renderowanie

Renderowanie UI polega na generowaniu ramki z aplikacji i wyświetleniu jej na ekranie. Aby zapewnić płynną interakcję użytkownika z aplikacją, aplikacja musi renderować klatki w czasie krótszym niż 16 ms, aby uzyskać 60 klatek na sekundę. Aby dowiedzieć się, dlaczego preferujemy 60 kl./s, przeczytaj artykuł Wzorce wydajności na Androidzie: dlaczego 60 kl./s?. Jeśli próbujesz uzyskać 90 kl./s, okno spada do 11 ms, a 120 – 8 ms.

Przekroczenie tego okna o 1 ms nie oznacza, że klatka wyświetli się po 1 ms, ale Choreographer całkowicie ją pomija. Jeśli Twoja aplikacja wolno renderuje się w interfejsie, system jest zmuszony pomijać klatki, a użytkownik widzi zacinanie się w aplikacji. Nazywamy to zacinaniem. Na tej stronie dowiesz się, jak zdiagnozować i naprawić zacięcie.

Jeśli tworzysz gry, które nie korzystają z systemu View, możesz pominąć tag Choreographer. W tym przypadku biblioteka tempa klatek pomaga grom w formatach OpenGL i Vulkan uzyskać płynne renderowanie i prawidłowe tempo klatek na Androidzie.

Aby poprawić jakość aplikacji, Android automatycznie monitoruje ją pod kątem problemów i wyświetla informacje w panelu Android Vitals. Informacje o sposobie zbierania danych znajdziesz w artykule Monitorowanie jakości technicznej aplikacji przy użyciu Android Vitals.

Zidentyfikuj zacięcie

Znalezienie w aplikacji kodu, który powoduje zacinanie, może być trudne. W tej sekcji opisujemy 3 metody identyfikowania zacięć:

Inspekcja wizualna pozwala w ciągu kilku minut prześledzić wszystkie przypadki użycia aplikacji, ale nie podaje tylu szczegółów jak Systrace. Więcej szczegółów znajdziesz w sekcji Systrace. Jeśli jednak uruchomisz Systrace do wszystkich zastosowań w aplikacji, może się to stać zbyt dużą ilością danych, które trudno analizować. Zarówno kontrola wizualna, jak i aplikacja Systrace wykrywają zacinanie na Twoim urządzeniu lokalnym. Jeśli nie możesz odtworzyć zacięcia na urządzeniach lokalnych, możesz utworzyć niestandardowe monitorowanie wydajności, aby mierzyć określone części aplikacji na urządzeniach działających w terenie.

Oględziny wizualne

Inspekcja wizualna pomaga zidentyfikować przypadki użycia, które powodują zacinanie. Aby przeprowadzić kontrolę wizualną, otwórz aplikację, przejrzyj ją ręcznie i sprawdź, czy nie ma zacięć w interfejsie.

Oto kilka wskazówek dotyczących kontroli wizualnej:

  • Uruchom wersję aplikacji – lub przynajmniej wersję niemożliwą do debugowania. Środowisko wykonawcze ART wyłącza kilka ważnych optymalizacji na potrzeby obsługi funkcji debugowania, więc upewnij się, że patrzysz na wersję przykładową, którą widzi użytkownik.
  • Włącz renderowanie GPU profilu. W ramach renderowania GPU w ramach tej funkcji wyświetlają się na ekranie paski pokazujące, ile czasu zajmuje wyrenderowanie klatek interfejsu w stosunku do wartości referencyjnej, która wynosi 16 ms na klatkę. Każdy słupek zawiera kolorowe komponenty, które mapują się na etap w procesie renderowania, dzięki czemu możesz sprawdzić, który fragment zajmuje najdłuższy czas. Jeśli np. ilość czasu poświęca się na obsługę danych wejściowych w ramce, przyjrzyj się kodowi aplikacji, który obsługuje dane wejściowe przez użytkownika.
  • Przejrzyj komponenty, które są typowymi źródłami zacięć, np. RecyclerView.
  • Uruchom aplikację „na zimno”.
  • Aby problem nie ustąpił, uruchom aplikację na wolniejszym urządzeniu.

Gdy znajdziesz przypadki użycia, które powodują zacinanie się, możesz zorientować się, co jest przyczyną zacinania się w aplikacji. Jeśli potrzebujesz więcej informacji, możesz użyć Systrace, aby dokładniej zbadać przyczynę.

Systrace

Choć Systrace to narzędzie, które pokazuje, co robi całe urządzenie, może być przydatne do wykrywania zacięć w aplikacji. Systrace ma minimalny narzut pracy, więc możesz odczuć realistyczny zmęczenie podczas instrumentacji.

Zarejestruj log czasu w Systrace podczas wykonywania na urządzeniu problematycznego zastosowania. Instrukcje korzystania z Systrace znajdziesz w artykule Rejestrowanie logu czasu systemu w wierszu poleceń. System Systrace jest podzielony na procesy i wątki. Poszukaj procesu aplikacji w Systrace, który wygląda mniej więcej tak jak na ilustracji 1.

Przykład aplikacji Systrace
Rysunek 1. Przykład Systrace.

Przykładowy plik Systrace na rys. 1 zawiera następujące informacje umożliwiające identyfikację dziurka:

  1. Aplikacja Systrace pokazuje, kiedy każda ramka jest rysowana. Każda klatka jest oznaczona kolorami, aby wyróżnić powolne renderowanie. Pomaga to dokładniej znajdować pojedyncze nieregularne klatki niż kontrola wizualna. Więcej informacji znajdziesz w artykule o sprawdzaniu ramek i alertów interfejsu.
  2. Systrace wykrywa problemy w aplikacji i wyświetla alerty zarówno w poszczególnych ramkach, jak i w panelu alertów. Najlepiej postępuj zgodnie z instrukcjami podanymi w alercie.
  3. Elementy platformy i bibliotek Androida, takie jak RecyclerView, zawierają znaczniki śledzenia. Tak więc oś czasu systrace wskazuje, kiedy te metody są wykonywane w wątku UI i jak długo trwa ich wykonanie.

Po sprawdzeniu danych wyjściowych Systrace może się okazać, że w Twojej aplikacji są metody, które prawdopodobnie powodują zacinanie. Jeśli np. oś czasu wskazuje, że spowolnione klatki są spowodowane długim czasem działania RecyclerView, możesz dodać niestandardowe zdarzenia logu czasu do odpowiedniego kodu i ponownie uruchomić Systrace, aby uzyskać więcej informacji. W nowej wersji Systrace oś czasu pokazuje, kiedy są wywoływane metody aplikacji i ile czasu zajmuje ich wykonanie.

Jeśli w Systrace nie znajdziesz szczegółowych informacji o tym, dlaczego działanie wątków UI trwa długo, użyj Profilu CPU w Androidzie, aby zarejestrować próbkowany lub instrumentowany log czasu. Ogólnie ślady metod nie sprawdzają się w identyfikowaniu zacięć, ponieważ generują fałszywie pozytywne zacięcia z powodu dużych nakładów pracy i nie widzą, kiedy wątki są uruchomione, a które zablokowane. Ślady metody mogą jednak pomóc w znalezieniu metod w aplikacji, które zajmują najwięcej czasu. Po zidentyfikowaniu tych metod dodaj znaczniki śledzenia i ponownie uruchom aplikację Systrace, aby sprawdzić, czy są one przyczyną zacinania.

Więcej informacji znajdziesz w artykule o usłudze Systrace.

Monitorowanie niestandardowych wyników

Jeśli nie możesz odtworzyć zacięcia na urządzeniu lokalnym, możesz utworzyć niestandardowe monitorowanie wydajności aplikacji, aby ułatwić identyfikację źródła zacięć na zarejestrowanych urządzeniach.

W tym celu za pomocą FrameMetricsAggregator zbieraj czasy renderowania klatek z określonych części aplikacji oraz zapisuj i analizuj dane za pomocą Monitorowania wydajności Firebase.

Więcej informacji znajdziesz w artykule Pierwsze kroki z Monitorowaniem wydajności na Androida.

Zablokowane klatki

Zablokowane klatki to klatki interfejsu, których renderowanie trwa dłużej niż 700 ms. Jest to problem, ponieważ aplikacja zawiesza się i nie reaguje na działania użytkownika przez prawie sekundę podczas renderowania ramki. Aby zapewnić płynność interfejsu, zalecamy zoptymalizowanie aplikacji pod kątem renderowania klatki w ciągu 16 ms. Jednak podczas uruchamiania aplikacji lub przechodzenia na inny ekran jest to normalne, że początkowa klatka trwa dłużej niż 16 ms, ponieważ aplikacja musi zwiększać liczbę widoków, rozmieszczać elementy na ekranie i rozpoczynać pierwsze rysowanie od zera. Dlatego Android śledzi zablokowane klatki niezależnie od powolnego renderowania. Renderowanie żadnej klatki w aplikacji nie powinno trwać dłużej niż 700 ms.

Aby pomóc Ci poprawić jakość aplikacji, Android automatycznie monitoruje aplikację pod kątem zablokowanych klatek i wyświetla te informacje w panelu Android Vitals. Informacje o sposobie zbierania danych znajdziesz w artykule Monitorowanie jakości technicznej aplikacji przy użyciu Android Vitals.

Zablokowane klatki to ekstremalna forma powolnego renderowania, więc procedura diagnozowania i rozwiązywania problemów jest taka sama.

Śledzenie zacięć

Funkcja FrameTimeline w Perfetto pomaga w śledzeniu spowolnionych lub zablokowanych klatek.

Związek między spowolnionymi klatkami, zablokowanymi klatkami i błędami ANR

Spowolnione klatki, zablokowane klatki i błędy ANR to różne rodzaje zacięć, z którymi może się spotykać aplikacja. W poniższej tabeli znajdziesz te różnice.

Spowolnione klatki Zablokowane klatki Błędy ANR
Czas renderowania Od 16 do 700 ms Od 700 ms do 5 s Ponad 5 s
Widoczny obszar wpływu na użytkowników
  • RecyclerView – nagle reaguje na przewijanie
  • na ekranach ze złożonymi animacjami, które nie animują się prawidłowo.
  • Podczas uruchamiania aplikacji
  • Przechodzenie z jednego ekranu na inny, np. przejście ekranu
  • Gdy aktywność jest na pierwszym planie, aplikacja nie odpowiedziała w ciągu 5 sekund na zdarzenie wejściowe lub BroadcastReceiver (np. naciśnięcie klawisza lub kliknięcie ekranu).
  • Chociaż na pierwszym planie nie ma żadnej aktywności, BroadcastReceiver nie zakończyło się jeszcze w krótkim czasie.

Oddzielne śledzenie spowolnionych i zablokowanych klatek

Podczas uruchamiania aplikacji lub przełączania się na inny ekran wyświetlenie początkowej klatki trwa dłużej niż 16 ms. Aplikacja musi zwiększać widok, rozmieszczać elementy na ekranie i rozpoczynać początkowe rysowanie od zera.

Sprawdzone metody określania priorytetów i rozwiązywania problemów

Szukając rozwiązania zacięcia w aplikacji, pamiętaj o tych sprawdzonych metodach:

  • Identyfikowanie i rozwiązywanie problemów najłatwiejszych do odtworzenia.
  • Nadaj priorytet błądom ANR. Wolne lub zablokowane klatki mogą powodować powolne działanie aplikacji, natomiast błędy ANR powodują, że aplikacja przestaje odpowiadać.
  • Powolne renderowanie jest trudne do odtworzenia, ale możesz zacząć od usunięcia zablokowanych klatek o długości 700 ms. Najczęściej dzieje się tak podczas uruchamiania lub zmieniania ekranu aplikacji.

Jak naprawić zacięcie

Aby naprawić zacinanie, sprawdź, które klatki nie kończą się w 16 ms, i poszukaj błędów. Sprawdź, czy w przypadku niektórych klatek rozmiar Record View#draw lub Layout nie trwa zbyt długo. Opis tych i innych problemów znajdziesz w artykule o częstych źródłach zacinania.

Aby uniknąć zacięć, długotrwałe zadania uruchamiaj asynchronicznie poza wątkiem interfejsu użytkownika. Zawsze sprawdzaj, w jakim wątku działa Twój kod, i zachowuj ostrożność podczas publikowania w wątku głównym mniej prostych zadań.

Jeśli masz złożony i ważny interfejs główny aplikacji, np. scentralizowaną listę przewijaną, rozważ pisanie testów instrumentacji, które mogą automatycznie wykrywać powolne renderowanie i często uruchamiać te testy, aby zapobiec regresjom.

Najczęstsze źródła zacięć

W sekcjach poniżej opisujemy typowe źródła zacinania się w aplikacjach korzystających z systemu View oraz sprawdzone metody ich radzenia sobie z nimi. Informacje o rozwiązywaniu problemów z wydajnością Jetpack Compose znajdziesz w artykule na temat wydajności tej usługi.

Przewijane listy

Funkcje ListView (a zwłaszcza RecyclerView) są powszechnie używane w przypadku skomplikowanych list przewijanych, które są najbardziej narażone na zacinanie. Oba zawierają znaczniki Systrace, dzięki czemu możesz użyć Systrace, aby sprawdzić, czy przyczyniają się one do zacięć w aplikacji. Przekaż argument wiersza poleceń -a <your-package-name>, aby wyświetlić sekcje śledzenia w RecyclerView oraz wszystkie dodane znaczniki śledzenia. Jeśli to możliwe, postępuj zgodnie ze wskazówkami dotyczącymi alertów wygenerowanych w danych wyjściowych Systrace. W sekcji Systrace możesz kliknąć sekcje ze śledzeniem RecyclerView, aby zobaczyć, co RecyclerView robi.

RecyclerView: notificationDataSetChanged()

Jeśli widzisz, że każdy element w RecyclerView się powtarza, a następnie jest ponownie ułożony i narysowany w jednej ramce, sprawdź, czy nie wywołujesz funkcji notifyDataSetChanged(), setAdapter(Adapter) ani swapAdapter(Adapter, boolean) w przypadku niewielkich aktualizacji. Te metody sygnalizują, że w całej liście zostały wprowadzone zmiany, i wyświetlają się w Systrace jako RV FullInvalidate. Zamiast tego użyj narzędzia SortedList lub DiffUtil, aby generować minimalne aktualizacje po zmianie lub dodaniu treści.

Weźmy za przykład aplikację, która otrzymuje z serwera nową wersję listy wiadomości. Gdy opublikujesz te informacje na karcie Adapter, możesz wywołać notifyDataSetChanged(), jak pokazano w tym przykładzie:

Kotlin

fun onNewDataArrived(news: List<News>) {
    myAdapter.news = news
    myAdapter.notifyDataSetChanged()
}

Java

void onNewDataArrived(List<News> news) {
    myAdapter.setNews(news);
    myAdapter.notifyDataSetChanged();
}

Wadą jest to, że istnieje drobna zmiana, np. dodanie 1 elementu na górze, element RecyclerView nie jest rozpoznawany. Dlatego pojawia się prośba o usunięcie całego stanu elementu z pamięci podręcznej i konieczność ponownego powiązania wszystkiego.

Zalecamy użycie narzędzia DiffUtil, które oblicza i wysyła minimalną liczbę aktualizacji:

Kotlin

fun onNewDataArrived(news: List<News>) {
    val oldNews = myAdapter.items
    val result = DiffUtil.calculateDiff(MyCallback(oldNews, news))
    myAdapter.news = news
    result.dispatchUpdatesTo(myAdapter)
}

Java

void onNewDataArrived(List<News> news) {
    List<News> oldNews = myAdapter.getItems();
    DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news));
    myAdapter.setNews(news);
    result.dispatchUpdatesTo(myAdapter);
}

Aby poinformować DiffUtil, jak sprawdzać listy, zdefiniuj MyCallback jako implementację Callback.

RecyclerView: zagnieżdżone obiekty RecyclerView

Często zagnieżdża się wiele wystąpień właściwości RecyclerView, zwłaszcza w przypadku pionowej listy przewijanych w poziomie list. Przykładem mogą być siatki aplikacji na stronie głównej Sklepu Play. Dobrze się to robi, ale wiąże się to z mnóstwem widoków.

Jeśli podczas przewijania strony w dół zauważysz, że wiele elementów wewnętrznych ma nadmuchanie, sprawdź, czy dane RecyclerView.RecycledViewPool są udostępniane między wewnętrznymi (poziomymi) wystąpieniami strony RecyclerView. Domyślnie każdy element RecyclerView ma własną pulę elementów. Jeśli jednak na ekranie wyświetla się kilkanaście elementów itemViews w tym samym czasie, problem występuje, gdy nie można udostępnić elementu itemViews przez różne poziome listy, jeśli wszystkie wiersze zawierają podobne typy wyświetleń.

Kotlin

class OuterAdapter : RecyclerView.Adapter<OuterAdapter.ViewHolder>() {

    ...

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // Inflate inner item, find innerRecyclerView by ID.
        val innerLLM = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false)
        innerRv.apply {
            layoutManager = innerLLM
            recycledViewPool = sharedPool
        }
        return OuterAdapter.ViewHolder(innerRv)
    }
    ...

Java

class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
    RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool();

    ...

    @Override
    public void onCreateViewHolder(ViewGroup parent, int viewType) {
        // Inflate inner item, find innerRecyclerView by ID.
        LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(),
                LinearLayoutManager.HORIZONTAL);
        innerRv.setLayoutManager(innerLLM);
        innerRv.setRecycledViewPool(sharedPool);
        return new OuterAdapter.ViewHolder(innerRv);

    }
    ...

Jeśli chcesz przeprowadzić dalszą optymalizację, możesz też wywołać setInitialPrefetchItemCount(int) na LinearLayoutManager wewnętrznej stronie RecyclerView. Jeśli np.w wierszu zawsze masz 3,5 elementu, wywołaj innerLLM.setInitialItemPrefetchCount(4). Informuje to RecyclerView, że gdy na ekranie pojawi się poziomy wiersz, musi on spróbować pobrać z wyprzedzeniem elementy w środku, jeśli znajdzie czas na wątek interfejsu.

RecyclerView: zbyt duża inflacja lub tworzenie trwa za długo

W większości przypadków funkcja pobierania z wyprzedzeniem w RecyclerView może pomóc obejść koszt inflacji, wykonując pracę z wyprzedzeniem, gdy wątek UI jest nieaktywny. Jeśli zauważysz zawyżenie tCPM w klatce, a nie w sekcji z etykietą Pobieranie z wyprzedzeniem, upewnij się, że przeprowadzasz testy na obsługiwanym urządzeniu i używasz najnowszej wersji Biblioteki pomocy. Pobieranie z wyprzedzeniem jest obsługiwane tylko na urządzeniach z Androidem 5.0 API poziomu 21 i nowszych.

Jeśli często zauważasz nadwyrężenie, które powoduje zacinanie się, gdy na ekranie pojawiają się nowe elementy, sprawdź, czy nie masz więcej typów widoków danych, niż potrzebujesz. Im mniej typów widoków danych w elemencie RecyclerView, tym mniejsza inflacja, gdy na ekranie pojawiają się nowe typy elementów. Jeśli to możliwe, scal typy widoków danych. Jeśli w poszczególnych typach zmienia się tylko ikona, kolor lub fragment tekstu, możesz wprowadzić tę zmianę w czasie wiązania i uniknąć nadmiernego wzrostu, co jednocześnie zmniejsza wykorzystanie ilości pamięci aplikacji.

Jeśli masz dobre typy widoków, zastanów się nad obniżeniem kosztu inflacji. Zmniejszenie zbędnych widoków kontenera i widoków strukturalnych może pomóc. Rozważ utworzenie właściwości itemViews za pomocą funkcji ConstraintLayout, która pomoże ograniczyć liczbę widoków strukturalnych.

Jeśli chcesz dodatkowo zoptymalizować wydajność, a Twoje hierarchie elementów są proste i nie potrzebujesz skomplikowanych funkcji tworzenia motywów i stylów, rozważ samodzielne wywoływanie konstruktorów. Często jednak nie warto rezygnować z utraty prostoty i funkcji kodu XML.

RecyclerView: powiązanie trwa zbyt długo

Powiązanie (czyli onBindViewHolder(VH, int)) musi być proste, a w przypadku wszystkich elementów oprócz najbardziej złożonych zajmuje znacznie mniej niż 1 milisekundę. Musi pobierać stare elementy obiektu Java (POJO) z wewnętrznych danych elementu adaptera i wywoływać funkcje ustawiające w widokach w ViewHolder. Jeśli funkcja RV OnBindView zajmuje więcej czasu, sprawdź, czy wprowadzasz minimalną ilość pracy w kodzie powiązania.

Jeśli do przechowywania danych w adapterze używasz podstawowych obiektów POJO, możesz uniknąć zapisywania kodu powiązania w onBindViewHolder dzięki bibliotece powiązań danych.

RecyclerView lub ListView: układ lub rysowanie trwa za długo

W przypadku problemów z rysowaniem i układem przeczytaj sekcje Wydajność układu i Wydajność renderowania.

Widok listy: inflacja

Jeśli nie zachowasz ostrożności, możesz przypadkowo wyłączyć recykling w aplikacji ListView. Jeśli za każdym razem, gdy element wyświetla się na ekranie, zauważysz zawyżenie liczby, sprawdź, czy implementacja Adapter.getView() analizuje, ponownie wiąże i zwraca parametr convertView. Jeśli Twoja implementacja getView() zawsze się zwiększa, aplikacja nie korzysta z zalet recyklingu w tym regionie: ListView. Struktura getView() musi niemal zawsze wyglądać podobnie do tej implementacji:

Kotlin

fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply {
        // Bind content from position to convertView.
    }
}

Java

View getView(int position, View convertView, ViewGroup parent) {

    if (convertView == null) {
        // Only inflate if no convertView passed.
        convertView = layoutInflater.inflate(R.layout.my_layout, parent, false)
    }
    // Bind content from position to convertView.
    return convertView;
}

Skuteczność układu

Jeśli firma Systrace pokazuje, że segment Układ elementu Choreographer#doFrame działa za dużo lub zbyt często, oznacza to, że występują problemy z wydajnością układu. Wydajność układu aplikacji zależy od tego, która część hierarchii widoków ma zmieniające się parametry układu lub dane wejściowe.

Skuteczność układu: koszt

Jeśli segmenty są dłuższe niż kilka milisekund, możliwe, że w przypadku właściwości RelativeLayouts (lub weighted-LinearLayouts) trafiasz informacje o najgorszej skuteczności zagnieżdżania. Każdy z tych układów może aktywować wiele przebiegów pomiarów i układów elementów podrzędnych, więc zagnieżdżenie ich może spowodować zachowanie funkcji O(n^2) na głębokości zagnieżdżenia.

Staraj się unikać właściwości RelativeLayout lub funkcji wagi LinearLayout we wszystkich węzłach liściennych w hierarchii oprócz najniższych węzłów. Możesz to zrobić na kilka sposobów:

  • Zmień kolejność widoków strukturalnych.
  • Zdefiniuj niestandardową logikę układu. Konkretny przykład znajdziesz w sekcji Optymalizowanie hierarchii układu. Możesz spróbować przejść na wersję ConstraintLayout, która oferuje podobne funkcje, ale bez wad.

Skuteczność układu: częstotliwość

Układ powinien nastąpić, gdy na ekranie pojawi się nowa treść, na przykład gdy nowy element pojawi się na ekranie w RecyclerView. Jeśli w każdej klatce występuje znaczny układ, możliwe, że animujesz układ, co często powoduje pomijanie klatek.

Ogólnie animacje muszą działać zgodnie z tymi właściwościami rysowania View, takimi jak:

Wszystkie te elementy możesz zmienić znacznie taniej niż właściwości układu, takie jak dopełnienie czy marginesy. Znacznie tańsze jest też zmienić właściwości rysowania w widoku danych przez wywołanie metody ustawiającej, która wywołuje metodę invalidate(), a następnie draw(Canvas) w następnej ramce. W ten sposób ponownie rejestrowane są operacje rysowania dla widoku, który został unieważniony i jest zwykle znacznie tańszy niż w przypadku układu.

Wydajność renderowania

Interfejs Androida działa w 2 etapach:

  • Zarejestruj polecenie View#draw w wątku interfejsu, który uruchamia metodę draw(Canvas) w każdym unieważnionym widoku i może wywoływać wywołania w widokach niestandardowych lub w Twoim kodzie.
  • DrawFrame w RenderThread, który uruchamia się w natywnym języku RenderThread, ale działa na podstawie pracy wygenerowanej w fazie Record View#draw.

Wydajność renderowania: wątek UI

Jeśli działanie Record View#draw trwa bardzo długo, w wątku interfejsu malowana jest często bitmapa. Obrazowanie na bitmapie wykorzystuje renderowanie procesora, więc w miarę możliwości należy unikać takich sytuacji. Aby sprawdzić, czy to jest przyczyną problemu, możesz użyć śledzenia metod za pomocą programu profilującego procesorów w Androidzie.

Obraz do bitmapy jest często malowany, gdy aplikacja chce udekorować bitmapę przed jej wyświetleniem. Czasami jest potrzebna ozdoba, na przykład zaokrąglone narożniki:

Kotlin

val paint = Paint().apply {
    isAntiAlias = true
}
Canvas(roundedOutputBitmap).apply {
    // Draw a round rect to define the shape:
    drawRoundRect(
            0f,
            0f,
            roundedOutputBitmap.width.toFloat(),
            roundedOutputBitmap.height.toFloat(),
            20f,
            20f,
            paint
    )
    paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
    // Multiply content on top to make it rounded.
    drawBitmap(sourceBitmap, 0f, 0f, paint)
    setBitmap(null)
    // Now roundedOutputBitmap has sourceBitmap inside, but as a circle.
}

Java

Canvas bitmapCanvas = new Canvas(roundedOutputBitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
// Draw a round rect to define the shape:
bitmapCanvas.drawRoundRect(0, 0,
        roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
// Multiply content on top to make it rounded.
bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint);
bitmapCanvas.setBitmap(null);
// Now roundedOutputBitmap has sourceBitmap inside, but as a circle.

Jeśli właśnie wykonujesz tę czynność w wątku interfejsu, zamiast tego możesz zrobić to na wątku dekodowania w tle. W niektórych przypadkach, tak jak w poprzednim przykładzie, możesz nawet wykonać pracę w czasie rysowania. Jeśli więc Twój kod Drawable lub View wygląda mniej więcej tak:

Kotlin

fun setBitmap(bitmap: Bitmap) {
    mBitmap = bitmap
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawBitmap(mBitmap, null, paint)
}

Java

void setBitmap(Bitmap bitmap) {
    mBitmap = bitmap;
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawBitmap(mBitmap, null, paint);
}

Możesz zastąpić go następującym kodem:

Kotlin

fun setBitmap(bitmap: Bitmap) {
    shaderPaint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawRoundRect(0f, 0f, width, height, 20f, 20f, shaderPaint)
}

Java

void setBitmap(Bitmap bitmap) {
    shaderPaint.setShader(
            new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint);
}

Możesz to również zrobić dla ochrony tła, np. rysując gradient na bitmapie, czy filtrować obrazy za pomocą ColorMatrixColorFilter – 2 inne typowe operacje wykonuje się przy modyfikowaniu map bitowych.

Jeśli rysujesz na mapie bitowej z innego powodu – być może używasz jej jako pamięci podręcznej – spróbuj narysować ją do akceleracji sprzętowej Canvas przekazanej bezpośrednio do View lub Drawable. W razie potrzeby możesz też wywołać metodę setLayerType() i LAYER_TYPE_HARDWARE, aby buforować złożone dane wyjściowe renderowania, ale nadal korzystać z renderowania za pomocą GPU.

Wydajność renderowania: RenderThread

Niektóre operacje Canvas są tanie, ale wywołują kosztowne obliczenia w RenderThread. Systrace zwykle informuje o tym za pomocą alertów.

Animowanie dużych ścieżek

Po wywołaniu Canvas.drawPath() akceleracji sprzętowej Canvas przekazanej do View Android rysuje te ścieżki najpierw na procesorze, a następnie przesyła je do GPU. Jeśli masz duże ścieżki, unikaj edytowania ich w kolejnych klatkach, aby można je było efektywnie zapisywać i rysować w pamięci podręcznej. Funkcje drawPoints(), drawLines() i drawRect/Circle/Oval/RoundRect() są skuteczniejsze i lepsze w użyciu, nawet jeśli używasz większej liczby wywołań przyciągających.

Canvas.clipPath

clipPath(Path) powoduje kosztowne przycinanie i zasadniczo należy tego unikać. Jeśli to możliwe, rysuj kształty zamiast przycinać je do nieprostokątnych. Działa lepiej i obsługuje anti-aliasing. Na przykład to wywołanie clipPath można wyrazić inaczej:

Kotlin

canvas.apply {
    save()
    clipPath(circlePath)
    drawBitmap(bitmap, 0f, 0f, paint)
    restore()
}

Java

canvas.save();
canvas.clipPath(circlePath);
canvas.drawBitmap(bitmap, 0f, 0f, paint);
canvas.restore();

Przedstaw poprzedni przykład w ten sposób:

Kotlin

paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
// At draw time:
canvas.drawPath(circlePath, mPaint)

Java

// One time init:
paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
// At draw time:
canvas.drawPath(circlePath, mPaint);
Przesłane bitmapy

Android wyświetla bitmapy jako tekstury OpenGL, a przy pierwszym wyświetleniu w ramce bitmapy jest ona przesyłana do GPU. Możesz ją zobaczyć w Systrace w postaci przesłanego tekstury(identyfikator) – szerokość x wysokość. Może to potrwać kilka milisekund, jak pokazano na Rysunku 2, ale konieczne jest wyświetlenie obrazu przy użyciu GPU.

Jeśli zajmuje to dużo czasu, najpierw sprawdź szerokość i wysokość w śladzie. Upewnij się, że wyświetlana bitmapa nie jest znacznie większa od obszaru na ekranie. Jeśli tak, marnuje to czas i pamięć przy przesyłaniu. Ogólnie biblioteki wczytywania bitmap zapewniają możliwość żądania mapy bitowej o odpowiednim rozmiarze.

W Androidzie 7.0 kod wczytywania map bitowych (zwykle jest wykonywany przez biblioteki) może wywołać metodę prepareToDraw(), aby wyzwolić wcześniejsze przesłanie, zanim zajdzie taka potrzeba. Dzięki temu przesyłanie będzie miało miejsce wcześniej, gdy RenderThread jest nieaktywny. Możesz to zrobić po zdekodowaniu lub po powiązaniu bitmapy z widokiem, o ile znasz tę mapę bitową. W idealnej sytuacji biblioteka wczytywania bitmap robi to za Ciebie, ale jeśli samodzielnie zarządzasz własną biblioteką lub chcesz mieć pewność, że nie trafisz na przesyłanie na nowszych urządzeniach, możesz wywołać prepareToDraw() we własnym kodzie.

Aplikacja poświęca dużo czasu w ramce na przesyłanie dużej bitmapy
Rysunek 2. Aplikacja poświęca dużo czasu w ramce na przesyłanie dużej mapy bitowej. Zmniejsz jego rozmiar lub aktywuj go na wczesnym etapie dekodowania za pomocą prepareToDraw().

Opóźnienia w harmonogramie wątków

Algorytm szeregowania wątków to część systemu operacyjnego Android, która określa, które wątki w systemie muszą działać, kiedy mają być uruchomione i jak długo.

Czasami zacinanie się występuje, gdy wątek UI aplikacji jest zablokowany lub nie działa. Systrace używa różnych kolorów, jak na ilustracji 3, aby wskazać, kiedy wątek jest uśpiony (szary), uruchamiany (niebieski: może się uruchamiać, ale harmonogram go jeszcze nie wybiera), aktywny (zielony) lub przerywany (czerwony lub pomarańczowy). Jest to bardzo przydatne do debugowania problemów z zacinaniem, spowodowanych opóźnieniami planowania wątków.

Wyróżnia okres, w którym wątek UI jest uśpiony
Rysunek 3. Wyróżniony okres, w którym wątek UI jest uśpiony.

Wywołania powiązane, czyli mechanizm komunikacji międzyprocesowej (IPC) na Androidzie, często powodują długie przerwy w wykonywaniu aplikacji. W nowszych wersjach Androida jest to jedna z najczęstszych przyczyn przerw w działaniu wątku interfejsu. Ogólnie rzecz biorąc, rozwiązaniem jest unikanie wywoływania funkcji, które wykonują wywołania Binder. Jeśli to niemożliwe, zapisz wartość w pamięci podręcznej lub przenieś pracę do wątków w tle. Gdy bazy kodu się powiększą, możesz przypadkowo dodać wywołanie Binder, wywołując metodę niskiego poziomu, jeśli nie zachowasz ostrożności. Możesz je jednak znaleźć i rozwiązać za pomocą śledzenia.

Jeśli masz transakcje powiązane, możesz przechwytywać ich stosy wywołań za pomocą tych poleceń adb:

$ adb shell am trace-ipc start
… use the app - scroll/animate ...
$ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
$ adb pull /data/local/tmp/ipc-trace.txt

Czasami wywołania, które wydają się nieszkodliwe, np. getRefreshRate(), mogą wywoływać wiążące się transakcje i powodować poważne problemy przy częstym wywoływaniu. Okresowe śledzenie pomaga w wykrywaniu i rozwiązywaniu problemów.

Pokazuje wątek interfejsu w trybie uśpienia z powodu transakcji segregatora w przebiegu kampera. Skoncentruj się na logice powiązania i użyj trace-ipc, aby śledzić i usuwać wywołania Binder.
Rysunek 4. Wątek UI jest uśpiony z powodu transakcji związanych z segregatorem w kamery. Używaj prostej logiki powiązania i użyj funkcji trace-ipc do śledzenia i usuwania wywołań wiążących.

Jeśli nie widzisz aktywności powiązania, ale nadal nie widzisz uruchomionego wątku UI, sprawdź, czy nie czekasz na blokadę lub inną operację z innego wątku. Zwykle wątek UI nie musi czekać na wyniki z innych wątków. Informacje w innych wątkach muszą być w nich publikowane.

Przydział obiektów i czyszczenie pamięci

Alokacja obiektów i czyszczanie pamięci to znacznie mniej problemów od momentu wprowadzenia ART jako domyślnego środowiska wykonawczego w Androidzie 5.0, ale te dodatkowe działania wiążą się z rozważeniem problemów na wątkach. Możesz przydzielać reklamy w odpowiedzi na rzadkie zdarzenie, które nie występuje wiele razy na sekundę, np. kliknięcie przycisku, ale pamiętaj, że każdy przydział wiąże się z określonym kosztem. Jeśli proces odbywa się w ścisłej pętli, która jest często wywoływana, rozważ uniknięcie przydziału w celu zmniejszenia obciążenia GC.

Systrace pokazuje, czy GC jest często uruchamiany, a Profil pamięci Androida pokazuje, skąd pochodzą przydziały. Jeśli w miarę możliwości unikasz przydziałów, zwłaszcza w oparciu o zapętlenie, zmniejszysz ryzyko problemów.

Wyświetla 94 ms GC w HeapTaskDaemon
Rysunek 5. 94 ms GC w wątku HeapTaskDaemon.

W najnowszych wersjach Androida GC zwykle działa w wątku w tle o nazwie HeapTaskDaemon. Znaczne kwoty przydziału mogą oznaczać więcej zasobów procesora przeznaczonych na GC, jak pokazano na rysunku 5.