Powolne renderowanie

Renderowanie interfejsu to generowanie klatki z aplikacji i wyświetlanie jej na ekranie. Aby zapewnić płynną interakcję użytkownika z aplikacją, musi ona renderować klatki w czasie krótszym niż 16 ms, aby osiągnąć 60 klatek na sekundę. Aby dowiedzieć się, dlaczego preferowane jest 60 klatek na sekundę, przeczytaj artykuł Android Performance Patterns: Why 60fps? (Wzorce wydajności Androida: dlaczego 60 klatek na sekundę?). Jeśli chcesz uzyskać 90 klatek na sekundę, to okno spada do 11 ms, a przy 120 klatkach na sekundę wynosi 8 ms.

Jeśli przekroczysz ten przedział o 1 ms, nie oznacza to, że klatka zostanie wyświetlona z opóźnieniem 1 ms, ale Choreographer całkowicie ją pominie. Jeśli aplikacja ma problemy z powolnym renderowaniem interfejsu, system musi pomijać klatki, a użytkownik zauważa przeskoki w aplikacji. Jest to tzw. zacięcie. Na tej stronie dowiesz się, jak diagnozować i naprawiać zacinanie się.

Jeśli tworzysz gry, które nie korzystają z systemu View, możesz pominąć krok Choreographer. W takim przypadku biblioteka Frame Pacing pomaga grom OpenGLVulkan osiągnąć płynne renderowanie i prawidłowe tempo klatek na Androidzie.

Wykrywanie zacięć

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

Kontrola wizualna pozwala w kilka minut przejrzeć wszystkie przypadki użycia w aplikacji, ale nie dostarcza tylu szczegółów co Systrace. Systrace zawiera więcej szczegółów, ale jeśli uruchomisz to narzędzie dla wszystkich przypadków użycia w aplikacji, możesz otrzymać tak dużo danych, że trudno będzie je przeanalizować. Zarówno kontrola wizualna, jak i Systrace wykrywają zacinanie się na urządzeniu lokalnym. Jeśli nie możesz odtworzyć zacięć na urządzeniach lokalnych, możesz utworzyć niestandardowe monitorowanie wydajności, aby mierzyć wydajność określonych części aplikacji na urządzeniach używanych w terenie.

Kontrola wizualna

Kontrola wizualna pomaga zidentyfikować przypadki użycia, które powodują zacinanie się animacji. Aby przeprowadzić kontrolę wizualną, otwórz aplikację i ręcznie przejrzyj różne jej części, szukając zacięć w interfejsie.

Oto kilka wskazówek dotyczących przeprowadzania kontroli wizualnych:

  • Uruchom wersję aplikacji, która jest przeznaczona do publikacji lub przynajmniej nie jest przeznaczona do debugowania. Środowisko wykonawcze ART wyłącza kilka ważnych optymalizacji, aby obsługiwać funkcje debugowania, więc upewnij się, że widzisz coś podobnego do tego, co widzi użytkownik.
  • Włącz profil renderowania GPU. Profil renderowania GPU wyświetla na ekranie słupki, które wizualnie przedstawiają, ile czasu zajmuje renderowanie klatek okna interfejsu w porównaniu z wartością referencyjną 16 ms na klatkę. Każdy słupek ma kolorowe komponenty, które odpowiadają etapowi w potoku renderowania, dzięki czemu możesz zobaczyć, która część zajmuje najwięcej czasu. Jeśli na przykład ramka spędza dużo czasu na obsługę danych wejściowych, przyjrzyj się kodowi aplikacji, który obsługuje dane wejściowe użytkownika.
  • Sprawdź komponenty, które są częstymi źródłami zacięć, takie jak RecyclerView.
  • Uruchom aplikację w ramach zimnego startu.
  • Uruchom aplikację na wolniejszym urządzeniu, aby spotęgować problem.

Gdy znajdziesz przypadki użycia, które powodują zacinanie się, możesz mieć dobre pojęcie o tym, co powoduje zacinanie się w aplikacji. Jeśli potrzebujesz więcej informacji, możesz użyć narzędzia Systrace, aby dokładniej zbadać przyczynę.

Systrace

Systrace to narzędzie, które pokazuje, co robi całe urządzenie, ale może być przydatne do identyfikowania zacięć w aplikacji. Systrace ma minimalne obciążenie systemu, więc podczas instrumentacji możesz doświadczyć realistycznych zacięć.

Zarejestruj ślad za pomocą narzędzia Systrace podczas wykonywania na urządzeniu przypadku użycia z problemami z płynnością. Instrukcje korzystania z narzędzia Systrace znajdziesz w artykule Rejestrowanie śladu systemowego w wierszu poleceń. Systrace jest podzielony według procesów i wątków. W narzędziu Systrace poszukaj procesu swojej aplikacji. Powinien wyglądać podobnie do tego na rysunku 1.

Przykład Systrace
Rysunek 1. Przykład śledzenia systemowego.

Przykład Systrace na rysunku 1 zawiera te informacje, które pomagają zidentyfikować zacięcia:

  1. Narzędzie Systrace pokazuje, kiedy każda klatka jest rysowana, i oznacza ją kolorem, aby wyróżnić długie czasy renderowania. Dzięki temu możesz dokładniej niż podczas sprawdzania wizualnego znajdować pojedyncze klatki z zacięciami. Więcej informacji znajdziesz w artykule Sprawdzanie ramek i alertów interfejsu.
  2. Systrace wykrywa problemy w aplikacji i wyświetla alerty zarówno w poszczególnych klatkach, jak i w panelu alertów. Najlepiej jest postępować zgodnie z instrukcjami podanymi w alercie.
  3. Części platformy i bibliotek Androida, takie jak RecyclerView, zawierają znaczniki śledzenia. Oś czasu systrace pokazuje, kiedy te metody są wykonywane w wątku interfejsu i ile czasu zajmuje ich wykonanie.

Po przejrzeniu danych wyjściowych narzędzia Systrace możesz znaleźć w aplikacji metody, które Twoim zdaniem powodują zacinanie się. Jeśli na przykład oś czasu pokazuje, że powolna klatka jest spowodowana długim czasem RecyclerView, możesz dodać niestandardowe zdarzenia śledzenia do odpowiedniego kodu i ponownie uruchomić Systrace, aby uzyskać więcej informacji. W nowym narzędziu Systrace oś czasu pokazuje, kiedy wywoływane są metody aplikacji i jak długo trwa ich wykonanie.

Jeśli Systrace nie pokazuje szczegółów dotyczących tego, dlaczego praca wątku UI trwa tak długo, użyj profilera procesora Androida, aby zarejestrować ślad metody próbkowanej lub instrumentowanej. Ślady metod zwykle nie nadają się do identyfikowania zacięć, ponieważ generują fałszywie dodatnie zacięcia z powodu dużego narzutu. Nie widać też, kiedy wątki są uruchomione, a kiedy zablokowane. Ślady metod mogą jednak pomóc w określeniu, które metody w aplikacji zajmują najwięcej czasu. Po zidentyfikowaniu tych metod dodaj znaczniki Trace i ponownie uruchom Systrace, aby sprawdzić, czy te metody powodują zacinanie się.

Więcej informacji znajdziesz w artykule Omówienie narzędzia Systrace.

Niestandardowe monitorowanie wydajności

Jeśli nie możesz odtworzyć zacięć na urządzeniu lokalnym, możesz wbudować w aplikację niestandardowe monitorowanie wydajności, aby pomóc w identyfikowaniu źródła zacięć na urządzeniach w terenie.

Aby to zrobić, zbieraj czasy renderowania klatek z określonych części aplikacji za pomocą FrameMetricsAggregator oraz rejestruj i analizuj dane za pomocą Firebase Performance Monitoring.

Więcej informacji znajdziesz w artykule Pierwsze kroki z monitorowaniem skuteczności na Androidzie.

Zablokowane klatki

Zablokowane klatki to klatki interfejsu, których renderowanie trwa dłużej niż 700 ms. Jest to problem, ponieważ podczas renderowania klatki aplikacja wydaje się zawieszać i nie reaguje na dane wejściowe użytkownika przez prawie sekundę. Aby zapewnić płynność interfejsu, zalecamy optymalizację aplikacji pod kątem renderowania klatki w ciągu 16 ms. Jednak podczas uruchamiania aplikacji lub przechodzenia do innego ekranu narysowanie pierwszej klatki może zająć więcej niż 16 ms, ponieważ aplikacja musi od zera rozwinąć widoki, rozmieścić elementy na ekranie i wykonać początkowe rysowanie. Dlatego Android śledzi zablokowane klatki oddzielnie od powolnego renderowania. Renderowanie żadnej klatki w aplikacji nie powinno trwać dłużej niż 700 ms.

Zatrzymane klatki to ekstremalna forma powolnego renderowania, więc procedura diagnozowania i rozwiązywania problemu jest taka sama.

Śledzenie zacięć

FrameTimelinePerfetto może pomóc w śledzeniu powolnych lub zamrożonych klatek.

Zależność między spowolnionymi klatkami, zablokowanymi klatkami i błędami ANR

Spowolnione klatki, zamrożone klatki i błędy ANR to różne formy zacinania, które mogą wystąpić w Twojej aplikacji. W tabeli poniżej znajdziesz informacje o różnicach.

Spowolnione klatki Zablokowane klatki Błędy ANR
Czas renderowania Od 16 ms do 700 ms Od 700 ms do 5 s Więcej niż 5 s
Widoczny obszar wpływu na użytkownika
  • RecyclerView nagłe przewijanie;
  • Na ekranach ze złożonymi animacjami, które nie działają prawidłowo
  • Podczas uruchamiania aplikacji
  • Przechodzenie z jednego ekranu na drugi, np. przejście między ekranami
  • Gdy aktywność jest na pierwszym planie, aplikacja nie odpowiada na zdarzenie wejściowe lub BroadcastReceiver – np. naciśnięcie klawisza lub dotknięcie ekranu – w ciągu 5 sekund.
  • Gdy aplikacja nie jest aktywna na pierwszym planie, funkcja BroadcastReceiver nie została wykonana w rozsądnym czasie.

Osobne śledzenie spowolnionych i zablokowanych klatek

Podczas uruchamiania aplikacji lub przechodzenia do innego ekranu początkowa klatka może być rysowana dłużej niż 16 ms, ponieważ aplikacja musi rozwinąć widoki, rozmieścić elementy na ekranie i wykonać początkowe rysowanie od zera.

Sprawdzone metody ustalania priorytetów i rozwiązywania problemów z zacinaniem się

Jeśli chcesz rozwiązać problem z zacinaniem się aplikacji, pamiętaj o tych sprawdzonych metodach:

  • Identyfikuj i rozwiązuj najłatwiejsze do odtworzenia przypadki zacinania się.
  • Nadawaj priorytet błędom ANR. Wolne lub zawieszone klatki mogą sprawiać wrażenie, że aplikacja działa powoli, ale błędy ANR powodują, że przestaje ona odpowiadać.
  • Powolne renderowanie jest trudne do odtworzenia, ale możesz zacząć od wyeliminowania zablokowanych klatek trwających 700 ms. Najczęściej dzieje się tak podczas uruchamiania aplikacji lub przełączania ekranów.

Naprawianie zacięć

Aby rozwiązać problem z zacinaniem się, sprawdź, które klatki nie są wyświetlane w ciągu 16 ms, i poszukaj przyczyny problemu. Sprawdź, czy w niektórych klatkach Record View#draw lub Layout nie trwa nienormalnie długo. Więcej informacji o tych i innych problemach znajdziesz w artykule Typowe przyczyny zacinania się animacji.

Aby uniknąć zacięć, wykonuj długotrwałe zadania asynchronicznie poza wątkiem UI. Zawsze sprawdzaj, w którym wątku działa Twój kod, i zachowuj ostrożność, gdy przesyłasz do głównego wątku nietrywialne zadania.

Jeśli Twoja aplikacja ma złożony i ważny interfejs podstawowy, np. centralną listę przewijaną, rozważ napisanie testów instrumentacyjnych, które mogą automatycznie wykrywać długi czas renderowania i często je uruchamiać, aby zapobiegać regresjom.

Typowe źródła zacięć

W kolejnych sekcjach opisujemy typowe przyczyny zacinania się aplikacji korzystających z View systemu oraz sprawdzone metody radzenia sobie z nimi. Informacje o rozwiązywaniu problemów z wydajnością Jetpack Compose znajdziesz w artykule Wydajność Jetpack Compose.

Listy z możliwością przewijania

ListView – a zwłaszcza RecyclerView – są często używane w przypadku złożonych list przewijanych, które są najbardziej podatne na zacinanie się. Obie zawierają znaczniki Systrace, więc możesz użyć Systrace, aby sprawdzić, czy przyczyniają się do zacinania się aplikacji. Przekaż argument wiersza poleceń -a <your-package-name>, aby wyświetlić sekcje śledzenia w RecyclerView, a także dodane przez Ciebie znaczniki śledzenia. Jeśli to możliwe, postępuj zgodnie z wskazówkami w alertach wygenerowanych w danych wyjściowych narzędzia Systrace. W narzędziu Systrace możesz klikać sekcje śledzone przez RecyclerView, aby zobaczyć wyjaśnienie wykonywanej przez nie pracy.RecyclerView

RecyclerView: notifyDataSetChanged()

Jeśli widzisz, że każdy element w RecyclerView jest ponownie wiązany – a tym samym ponownie rozmieszczany i rysowany w jednej klatce – upewnij się, że nie wywołujesz funkcji notifyDataSetChanged(), setAdapter(Adapter) ani swapAdapter(Adapter, boolean) w przypadku niewielkich aktualizacji. Te metody sygnalizują, że w całej zawartości listy zaszły zmiany, i w narzędziu Systrace są widoczne jako RV FullInvalidate. Zamiast tego użyj SortedList lub DiffUtil, aby generować minimalne aktualizacje, gdy treść zostanie zmieniona lub dodana.

Rozważmy na przykład aplikację, która otrzymuje z serwera nową wersję listy treści informacyjnych. Gdy opublikujesz te informacje w Adapterze, 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ą tego rozwiązania jest to, że w przypadku drobnych zmian, np. dodania jednego elementu na początku, RecyclerView nie jest tego świadomy. Dlatego poleca się mu usunięcie całego stanu buforowanego elementu, co wymaga ponownego powiązania wszystkiego.

Zalecamy używanie DiffUtil, które oblicza i wysyła minimalne aktualizacje:

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ć Twoje listy, zdefiniuj MyCallback jako implementację Callback.

RecyclerView: zagnieżdżone widoki RecyclerView

Często zdarza się, że zagnieżdżonych jest wiele instancji elementu RecyclerView, zwłaszcza w przypadku pionowej listy przewijanych w poziomie list. Przykładem tego są siatki aplikacji na stronie głównej Sklepu Play. Może to być świetne rozwiązanie, ale wiąże się z dużą liczbą wyświetleń.

Jeśli po pierwszym przewinięciu strony w dół widzisz, że wiele elementów wewnętrznych się powiększa, sprawdź, czy udostępniasz RecyclerView.RecycledViewPool między wewnętrznymi (poziomymi) instancjami RecyclerView. Domyślnie każdy RecyclerView ma własną pulę produktów. Jeśli jednak na ekranie jednocześnie wyświetla się kilkanaścieitemViews, problematyczne jest to, żeitemViews nie można udostępniać w różnych listach poziomych, jeśli wszystkie wiersze zawierają podobne typy widoków.

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 jeszcze bardziej zoptymalizować działanie, możesz też wywołać setInitialPrefetchItemCount(int) wewnętrzną funkcję LinearLayoutManager wewnętrznego elementu RecyclerView. Jeśli na przykład zawsze masz widoczne 3,5 elementu w rzędzie, wywołaj funkcję innerLLM.setInitialItemPrefetchCount(4). Sygnalizuje to RecyclerView, że gdy wiersz poziomy ma się pojawić na ekranie, musi spróbować wstępnie pobrać elementy w nim zawarte, jeśli w wątku interfejsu użytkownika jest wolny czas.

RecyclerView: zbyt dużo tworzenia instancji lub tworzenie trwa zbyt długo

W większości przypadków funkcja wstępnego pobierania w RecyclerView może pomóc w ograniczeniu kosztów inflacji, wykonując pracę z wyprzedzeniem, gdy wątek interfejsu jest bezczynny. Jeśli widzisz wzrost w trakcie klatki, a nie w sekcji oznaczonej jako RV Prefetch, upewnij się, że testujesz na obsługiwanym urządzeniu i używasz najnowszej wersji biblioteki pomocy. Wstępne pobieranie jest obsługiwane tylko na Androidzie 5.0 (poziom interfejsu API 21) i nowszym.

Jeśli często widzisz, że inflacja powoduje zacinanie się obrazu, gdy na ekranie pojawiają się nowe elementy, sprawdź, czy nie masz więcej typów widoków niż potrzebujesz. Im mniej typów widoków w treści elementu RecyclerView, tym mniejsza inflacja jest potrzebna, gdy na ekranie pojawiają się nowe typy elementów. W miarę możliwości scalaj typy widoków, jeśli jest to uzasadnione. Jeśli między typami zmienia się tylko ikona, kolor lub fragment tekstu, możesz wprowadzić tę zmianę w momencie powiązania i uniknąć inflacji, co jednocześnie zmniejszy rozmiar aplikacji w pamięci.

Jeśli typy wyświetleń wyglądają dobrze, spróbuj obniżyć koszt inflacji. Pomocne może być ograniczenie niepotrzebnych wyświetleń kontenerów i elementów strukturalnych. Rozważ tworzenie itemViewsza pomocą ConstraintLayout, co może pomóc w ograniczeniu liczby wyświetleń strukturalnych.

Jeśli chcesz jeszcze bardziej zoptymalizować wydajność, a hierarchie produktów są proste i nie potrzebujesz złożonych funkcji motywów i stylów, rozważ samodzielne wywoływanie konstruktorów. Często jednak nie warto rezygnować z prostoty i funkcji XML.

RecyclerView: wiązanie trwa zbyt długo

Wiązanie, czyli onBindViewHolder(VH, int), musi być proste i zajmować znacznie mniej niż milisekundę w przypadku wszystkich elementów z wyjątkiem najbardziej złożonych. Musi pobierać zwykłe obiekty Java (POJO) z wewnętrznych danych produktu adaptera i wywoływać metody ustawiające w widokach w ViewHolder. Jeśli RV OnBindView trwa długo, sprawdź, czy w kodzie powiązania wykonujesz minimalną ilość pracy.

Jeśli do przechowywania danych w adapterze używasz podstawowych obiektów POJO, możesz całkowicie uniknąć pisania kodu powiązania w onBindViewHolder, korzystając z biblioteki powiązań danych.

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

W przypadku problemów z rysowaniem i układem zapoznaj się z sekcjami Wydajność układuWydajność renderowania.

ListView: Inflation

Jeśli nie zachowasz ostrożności, możesz przypadkowo wyłączyć recykling w ListView. Jeśli za każdym razem, gdy element pojawia się na ekranie, widzisz inflację, sprawdź, czy implementacja Adapter.getView() wykonuje operacje musing, re-binding i zwraca parametr convertView. Jeśli Twoja implementacja getView() zawsze powiększa widok, aplikacja nie korzysta z zalet recyklingu w ListView. Struktura getView() musi prawie zawsze być podobna 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;
}

Wydajność układu

Jeśli Systrace pokazuje, że segment LayoutChoreographer#doFrame jest zbyt obciążony lub zbyt często używany, 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 lub dane wejściowe układu.

Skuteczność układu: koszt

Jeśli segmenty są dłuższe niż kilka milisekund, może to oznaczać, że osiągasz najgorszą wydajność zagnieżdżania w przypadku RelativeLayouts lub weighted-LinearLayouts. Każdy z tych układów może wywoływać wiele przebiegów pomiaru i układu elementów podrzędnych, więc zagnieżdżanie ich może prowadzić do O(n^2) zachowania w zależności od głębokości zagnieżdżenia.

Staraj się unikać RelativeLayout lub funkcji wagiLinearLayout we wszystkich węzłach z wyjątkiem najniższych węzłów w hierarchii. Możesz to zrobić na kilka sposobów:

  • Zmień kolejność widoków strukturalnych.
  • Zdefiniuj niestandardową logikę układu. Konkretny przykład znajdziesz w artykule Optymalizowanie hierarchii układu. Możesz spróbować przejść na ConstraintLayout, która oferuje podobne funkcje, ale nie ma wad związanych ze skutecznością.

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

Układ powinien się zmieniać, gdy na ekranie pojawiają się nowe treści, np. gdy w RecyclerView pojawia się nowy element. Jeśli w każdej klatce następuje znacząca zmiana układu, prawdopodobnie animujesz układ, co może powodować utratę klatek.

Animacje muszą zwykle działać na właściwościach rysowania elementu View, takich jak:

Możesz je zmienić znacznie taniej niż właściwości układu, takie jak dopełnienie czy marginesy. Zwykle zmiana właściwości rysowania widoku przez wywołanie funkcji ustawiającej, która wywołuje invalidate(), a następnie draw(Canvas) w następnej klatce, jest też znacznie tańsza. Ponownie rejestruje operacje rysowania dla widoku, który jest unieważniony, i jest też zwykle znacznie tańszy niż układ.

Wydajność renderowania

Interfejs Androida działa w 2 fazach:

  • Record View#draw w wątku UI, który jest uruchamiany draw(Canvas) w przypadku każdego unieważnionego widoku i może wywoływać połączenia z widokami niestandardowymi lub z Twoim kodem.
  • DrawFrame na RenderThread, która działa na natywnym RenderThread, ale opiera się na pracy wygenerowanej w fazie Record View#draw.

Wydajność renderowania: wątek UI

Jeśli Record View#draw trwa długo, zwykle oznacza to, że na wątku UI rysowana jest mapa bitowa. Rysowanie na mapie bitowej wykorzystuje renderowanie na procesorze, więc w miarę możliwości unikaj tego. Aby sprawdzić, czy to jest problem, możesz użyć śledzenia metod za pomocą CPU Profilera Androida.

Rysowanie na mapie bitowej jest często wykonywane, gdy aplikacja chce ozdobić mapę bitową przed jej wyświetleniem. Czasami jest to ozdoba, taka jak dodanie zaokrąglonych rogów:

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 takie zadania wykonujesz w wątku UI, możesz zamiast tego wykonać je w wątku dekodowania w tle. W niektórych przypadkach, np. w powyższym przykładzie, możesz wykonać pracę w momencie losowania. Jeśli więc 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 go zastąpić tym:

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 też zrobić w przypadku ochrony tła, np. podczas rysowania gradientu na bitmapie, oraz filtrowania obrazu za pomocą ColorMatrixColorFilter – dwóch innych typowych operacji wykonywanych podczas modyfikowania bitmap.

Jeśli rysujesz na mapie bitowej z innego powodu – być może używasz jej jako pamięci podręcznej – spróbuj rysować bezpośrednio na przyspieszonym sprzętowo obiekcie Canvas przekazywanym do funkcji View lub Drawable. W razie potrzeby możesz też wywołać funkcję setLayerType() z parametrem LAYER_TYPE_HARDWARE w celu zapisania w pamięci podręcznej złożonych wyników renderowania i korzystania z renderowania na GPU.

Wydajność renderowania: RenderThread

Niektóre operacje Canvas są tanie w rejestrowaniu, ale wywołują kosztowne obliczenia na RenderThread. Systrace zwykle sygnalizuje takie sytuacje za pomocą alertów.

Animowanie dużych ścieżek

Gdy funkcja Canvas.drawPath() jest wywoływana na akcelerowanym sprzętowo obiekcie Canvas przekazanym do funkcji View, Android najpierw rysuje te ścieżki na procesorze, a następnie przesyła je do procesora graficznego. Jeśli masz duże ścieżki, unikaj edytowania ich klatka po klatce, aby można było je buforować i wydajnie rysować. drawPoints(), drawLines()drawRect/Circle/Oval/RoundRect() są wydajniejsze i lepiej się sprawdzają, nawet jeśli używasz większej liczby wywołań rysowania.

Canvas.clipPath

clipPath(Path) wywołuje kosztowne przycinanie i zwykle należy go unikać. Jeśli to możliwe, rysuj kształty zamiast przycinać do kształtów innych niż prostokąty. Działa lepiej i obsługuje wygładzanie krawędzi. Na przykład to wywołanieclipPath można zapisać 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();

Zamiast tego wyraź 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);
Przesyłanie bitmap

Android wyświetla mapy bitowe jako tekstury OpenGL. Gdy mapa bitowa jest wyświetlana w klatce po raz pierwszy, jest przesyłana do procesora graficznego. W narzędziu Systrace zobaczysz to jako Texture upload(id) width x height. Może to potrwać kilka milisekund, jak pokazano na rysunku 2, ale jest to konieczne, aby wyświetlić obraz za pomocą procesora graficznego.

Jeśli trwa to długo, najpierw sprawdź liczby szerokości i wysokości w śladzie. Upewnij się, że wyświetlana mapa bitowa nie jest znacznie większa niż obszar na ekranie, na którym jest wyświetlana. W takim przypadku marnujesz czas przesyłania i pamięć. Biblioteki wczytywania map bitowych zwykle umożliwiają żądanie mapy bitowej o odpowiednim rozmiarze.

W Androidzie 7.0 kod wczytywania bitmapy – zwykle wykonywany przez biblioteki – może wywoływać funkcję prepareToDraw(), aby wywołać wczesne przesyłanie przed jego potrzebą. Dzięki temu przesyłanie rozpocznie się wcześniej, gdy RenderThread jest w stanie bezczynności. Możesz to zrobić po dekodowaniu lub podczas wiązania mapy bitowej z widokiem, o ile znasz mapę bitową. Najlepiej, aby biblioteka wczytywania map bitowych robiła to za Ciebie, ale jeśli zarządzasz własną biblioteką lub chcesz mieć pewność, że nie przekroczysz limitu przesyłania na nowszych urządzeniach, możesz wywołać funkcję prepareToDraw() we własnym kodzie.

Aplikacja spędza dużo czasu na przesyłaniu dużej bitmapy w ramce
Rysunek 2. Aplikacja spędza dużo czasu w ramce, przesyłając dużą bitmapę. Możesz zmniejszyć jego rozmiar lub wywołać go wcześniej, gdy dekodujesz go za pomocą prepareToDraw().

Opóźnienia w planowaniu wątków

Harmonogram wątków to część systemu operacyjnego Androida, która decyduje, które wątki w systemie mają być uruchamiane, kiedy i na jak długo.

Czasami zacięcia występują, ponieważ wątek interfejsu aplikacji jest zablokowany lub nie działa. Systrace używa różnych kolorów, jak pokazano na rysunku 3, aby wskazać, kiedy wątek śpi (szary), jest gotowy do uruchomienia (niebieski: może działać, ale nie został jeszcze wybrany przez harmonogram), aktywnie działa (zielony) lub jest w stanie nieprzerwanego uśpienia (czerwony lub pomarańczowy). Jest to niezwykle przydatne do debugowania problemów z zacinaniem się, które są spowodowane opóźnieniami w planowaniu wątków.

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

Często wywołania mechanizmu Binder, czyli mechanizmu komunikacji międzyprocesowej (IPC) na Androidzie, powodują długie przerwy w działaniu aplikacji. W nowszych wersjach Androida jest to jedna z najczęstszych przyczyn zatrzymania wątku UI. Zazwyczaj rozwiązaniem jest unikanie wywoływania funkcji, które wykonują wywołania powiązań. Jeśli jest to nieuniknione, zapisz wartość w pamięci podręcznej lub przenieś pracę do wątków w tle. W przypadku większych baz kodu, jeśli nie zachowasz ostrożności, możesz przypadkowo dodać wywołanie usługi Binder, wywołując jakąś metodę niskiego poziomu. Możesz je jednak znaleźć i naprawić za pomocą śledzenia.

Jeśli masz transakcje w binderze, możesz przechwycić 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ę niegroźne, np. getRefreshRate(), mogą wywoływać transakcje powiązania i powodować duże problemy, gdy są często wywoływane. Okresowe śledzenie może pomóc w znalezieniu i rozwiązaniu tych problemów, gdy się pojawią.

Pokazuje wątek interfejsu uśpiony z powodu transakcji bindera w przewijaniu RV. Skup się na logice wiązania i używaj narzędzia trace-ipc do śledzenia i usuwania wywołań bindera.
Rysunek 4. Wątek interfejsu użytkownika jest uśpiony z powodu transakcji binder w przewijaniu RV. Zadbaj o prostą logikę wiązania i używaj trace-ipc, aby śledzić i usuwać wywołania narzędzia do wiązania.

Jeśli nie widzisz aktywności powiązania, ale wątek interfejsu nadal nie działa, 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. Inne wątki muszą publikować w nim informacje.

Przydzielanie obiektów i odśmiecanie pamięci

Alokacja obiektów i odśmiecanie pamięci (GC) są znacznie mniejszym problemem od czasu wprowadzenia ART jako domyślnego środowiska wykonawczego w Androidzie 5.0, ale nadal można obciążyć wątki dodatkową pracą. Możesz przydzielać pamięć w odpowiedzi na rzadkie zdarzenie, które nie występuje wiele razy na sekundę, np. kliknięcie przycisku przez użytkownika, ale pamiętaj, że każde przydzielenie wiąże się z kosztem. Jeśli jest w ciasnej pętli, która jest często wywoływana, rozważ uniknięcie alokacji, aby zmniejszyć obciążenie GC.

Systrace pokazuje, czy odśmiecanie pamięci jest uruchamiane często, a profiler pamięci Androida może pokazywać, skąd pochodzą przydziały pamięci. Jeśli w miarę możliwości unikasz alokacji, zwłaszcza w przypadku krótkich pętli, rzadziej będziesz mieć problemy.

Pokazuje 94-milisekundowe odśmiecanie pamięci w usłudze HeapTaskDaemon.
Rysunek 5. 94-milisekundowe odśmiecanie pamięci na wątku HeapTaskDaemon.

W najnowszych wersjach Androida odśmiecanie pamięci zwykle odbywa się w wątku w tle o nazwie HeapTaskDaemon. Znaczne ilości przydzielonej pamięci mogą oznaczać większe zużycie zasobów procesora na potrzeby odśmiecania, co widać na rysunku 5.