Wydajność i hierarchie widoków

Sposób zarządzania hierarchią obiektów View może mieć znaczący wpływ na działanie aplikacji. Na tej stronie znajdziesz informacje o tym, jak sprawdzić, czy hierarchia widoku nie powoduje spowolnienia działania aplikacji, oraz kilka strategii rozwiązywania problemów, które mogą się pojawić.

Ta strona dotyczy ulepszania układów opartych na View. Informacje o zwiększaniu wydajności Jetpack Compose znajdziesz w artykule Wydajność Jetpack Compose.

Skuteczność układu i pomiary

Proces renderowania obejmuje etap projektowania i pomiaru, podczas którego system odpowiednio umieszcza odpowiednie elementy w hierarchii widoku. Część measure tego etapu określa rozmiary i granice obiektów View. Część layout określa, gdzie na ekranie mają się znaleźć obiekty View.

Oba te etapy strumienia powodują niewielki koszt na wyświetlenie lub przetwarzany układ. W większości przypadków jest on minimalny i nie ma zauważalnego wpływu na wydajność. Może ona jednak być większa, gdy aplikacja dodaje lub usuwa obiekty View, np. gdy obiekt RecyclerView je odzyskuje lub ponownie używa. Koszty mogą być też większe, jeśli obiekt View wymaga zmiany rozmiaru, by spełnić jego wymagania. Jeśli na przykład aplikacja wywołuje SetText() w obiekcie View, który zawija tekst, View może wymagać zmiany rozmiaru.

Jeśli trwa to zbyt długo, może to uniemożliwić wyrenderowanie klatki w dozwolonym 16 ms, przez co klatki będą spadać i zaburzać animację.

Ponieważ nie możesz przenieść tych operacji na wątek roboczy (aplikacja musi przetwarzać je na głównym wątku), najlepiej je zoptymalizować, aby zajmowały jak najmniej czasu.

Zarządzanie złożonymi układami

Układy w Androidzie umożliwiają zagnieżdżanie obiektów interfejsu w hierarchii widoków. Może to też wiązać się z kosztem związanym z układem. Gdy aplikacja przetwarza obiekt na potrzeby układu, wykonuje ten sam proces na wszystkich elementach podrzędnych układu.

W przypadku skomplikowanego układu czasami koszt jest naliczany dopiero przy pierwszym obliczeniu układu przez system. Gdy na przykład aplikacja ponownie wykorzysta złożony element listy w obiekcie RecyclerView, system musi ułożyć wszystkie obiekty. W innym przykładzie łańcuch drobnych zmian może się przesunąć w górę łańcucha w stronę elementu nadrzędnego, aż dotrą one do obiektu, który nie ma wpływu na rozmiar elementu nadrzędnego.

Częstą przyczyną długiego czasu wczytywania układu jest to, że hierarchie obiektów View są w sobie zagnieżdżone. Każdy zagnieżdżony obiekt układu zwiększa koszt etapu układu. Im bardziej płaska jest Twoja hierarchia, tym mniej czasu zajmuje etap układu.

Zalecamy użycie Edytora układu do utworzenia ConstraintLayout zamiast RelativeLayout lub LinearLayout, ponieważ jest to zazwyczaj bardziej wydajne i zmniejsza zagnieżdżenie układów. Jednak w przypadku prostych układów, które można uzyskać za pomocą FrameLayout, zalecamy użycie FrameLayout.

Jeśli używasz klasy RelativeLayout, możesz osiągnąć ten sam efekt przy niższych kosztach, korzystając z zagnieżdżonych, nieważonych widoków LinearLayout. Jeśli jednak używasz zagnieżdżonych, ważonych widoków LinearLayout, koszt układu jest znacznie wyższy, ponieważ wymaga wielu przejść układu, jak wyjaśniono w następnej sekcji.

Zalecamy też użycie RecyclerView zamiast ListView, ponieważ pozwala to wykorzystać układy poszczególnych elementów listy, co jest bardziej wydajne i może poprawić wydajność przewijania.

Podwójne opodatkowanie

Zwykle platforma wykonuje etap układu lub pomiaru w jednym przejściu. Jednak w niektórych skomplikowanych przypadkach układu framework może musieć kilkakrotnie przetwarzać części hierarchii, które wymagają wielu przejść, aby można było je umieścić, zanim elementy zostaną ostatecznie umieszczone. Wykonanie więcej niż 1 iteracji układu i pomiaru to podwójne opodatkowanie.

Jeśli na przykład używasz kontenera RelativeLayout, który umożliwia umieszczanie obiektów View względem pozycji innych obiektów View, framework wykonuje tę sekwencję:

  1. Wykonuje przetwarzanie układu i wymiarów, podczas którego framework oblicza położenie i rozmiar każdego podrzędnego obiektu na podstawie żądania.
  2. Korzysta z tych danych, biorąc pod uwagę wagi obiektów, aby określić właściwą pozycję powiązanych widoków.
  3. Wykonuje drugi obrót układu, aby dokończyć pozycjonowanie obiektów.
  4. Przechodzi do następnego etapu procesu renderowania.

Im więcej poziomów ma Twoja hierarchia widoku, tym większe ryzyko pogorszenia wydajności.

Jak już wspomnieliśmy, układ ConstraintLayout jest zwykle wydajniejszy niż inne układy z wyjątkiem FrameLayout. Jest on mniej podatny na wielokrotne przejścia przez układ i w wielu przypadkach eliminuje potrzebę zagnieżdżania układów.

Z podwójnym opodatkowaniem mogą się też wiązać kontenery inne niż RelativeLayout. Przykład:

  • Widok LinearLayout może spowodować podwójne przejście przez układ i wymiar, jeśli zrobisz go poziomy. Podwójne ułożenie i pomiar mogą też wystąpić w orientacji pionowej, jeśli dodasz measureWithLargestChild. W takim przypadku framework może potrzebować drugiego przejścia, aby określić właściwe rozmiary obiektów.
  • GridLayout umożliwia również pozycjonowanie względne, ale zwykle pozwala uniknąć podwójnego opodatkowania przez wstępne przetwarzanie relacji pozycjonowania między widokami podrzędnymi. Jeśli jednak układ używa wag lub wypełnienia z klasą Gravity, utracona jest korzyść z wstępnego przetwarzania, a platforma będzie musiała wykonywać wiele kart, jeśli kontenerem jest RelativeLayout.

Wielokrotne przejścia przez proces tworzenia i pomiaru nie muszą obciążać wydajności. Mogą jednak stać się uciążliwe, jeśli znajdą się w niewłaściwym miejscu. Zachowaj ostrożność w sytuacjach, gdy kontener ma zastosowanie jeden z tych warunków:

  • Jest to główny element w hierarchii widoków.
  • Ma głęboką hierarchię widoków.
  • Na ekranie jest wiele jego instancji, podobnie jak elementów podrzędnych obiektu ListView.

Diagnozowanie problemów z hierarchią widoków

Wydajność układu to złożony problem, który obejmuje wiele aspektów. Poniższe narzędzia pomogą Ci zidentyfikować wąskie gardła wydajności. Niektóre narzędzia podają mniej jednoznaczne informacje, ale mogą zawierać przydatne wskazówki.

Perfetto

Perfetto to narzędzie, które dostarcza dane o skuteczności. Śledzenie na Androidzie możesz otworzyć w interfejsie Perfetto.

Profil renderowania GPU

Narzędzie Profilowanie renderowania GPU dostępne na urządzeniach z Androidem 6.0 (poziom interfejsu API 23) lub nowszym może dostarczyć konkretnych informacji o wąskich gardłach wydajności. To narzędzie pozwala sprawdzić, ile czasu zajmuje etap układania i pomiary w przypadku każdego kadru renderowania. Te dane mogą pomóc w diagnozowaniu problemów z wydajnością w czasie działania i określaniu, jakie problemy związane z układem i wymiarami wymagają rozwiązania.

W graficznej reprezentacji danych renderowanie GPU profilu używa koloru niebieskiego do reprezentowania czasu układania. Więcej informacji o korzystaniu z tego narzędzia znajdziesz w artykule Profilowanie szybkości renderowania przez GPU.

Lint

Narzędzie Lint w Android Studio może pomóc Ci w znalezieniu nieefektywności w hierarchii widoku. Aby użyć tego narzędzia, kliknij Analizuj > Sprawdź kod, jak pokazano na rysunku 1.

Rysunek 1. W Android Studio kliknij Zbadaj kod.

Informacje o różnych elementach układu znajdziesz w sekcji Android > Lint > Wydajność. Aby wyświetlić więcej szczegółów, kliknij każdy element, aby go rozwinąć i wyświetlić więcej informacji w panelu po prawej stronie ekranu. Rysunek 2 przedstawia przykład rozszerzonych informacji.

Rysunek 2. Wyświetlanie informacji o konkretnych problemach zidentyfikowanych przez narzędzie Lint.

Kliknięcie elementu powoduje wyświetlenie powiązanych z nim problemów w panelu po prawej stronie.

Więcej informacji o konkretnych tematach i problemach w tej dziedzinie znajdziesz w dokumentacji Lint.

Inspektor układu

Narzędzie Inspektor układu w Android Studio zapewnia wizualną reprezentację hierarchii widoków aplikacji. Jest to dobry sposób na poruszanie się po hierarchii aplikacji. Zapewnia ona czytelną wizualizację łańcucha nadrzędnego danego widoku i umożliwia sprawdzanie układów tworzonych przez aplikację.

Widoki w inspektorze układu mogą też pomóc w identyfikacji problemów z wydajnością wynikających z podwójnego opodatkowania. Możesz też zidentyfikować długie łańcuchy zagnieżdżonych układów lub obszary układu z dużą liczbą zagnieżdżonych elementów podrzędnych, które mogą powodować koszty związane z wydajnością. W takich przypadkach etapy układu i pomiarów mogą być kosztowne i powodować problemy z wydajnością.

Więcej informacji znajdziesz w artykule Debugowanie układu za pomocą narzędzia do sprawdzania i weryfikacji układu.

Rozwiązywanie problemów z hierarchią widoków

Podstawowa koncepcja rozwiązywania problemów z wydajnością, które powstają w wyniku hierarchii widoków, może być w praktyce trudna. Zapobieganie nakładaniu kar za wydajność przez hierarchie widoków wiąże się z wyrównaniem hierarchii widoków i ograniczeniem podwójnego opodatkowania. W tej sekcji omówiono strategie służące do osiągania tych celów.

Usuń zbędne układy zagnieżdżone

ConstraintLayout to biblioteka Jetpack z wieloma różnymi mechanizmami pozycjonowania widoków w układzie. Dzięki temu nie musisz umieszczać jednego ConstaintLayout w innym, co może pomóc w spłaszczeniu hierarchii widoku. Zbiorcze układy ConstraintLayout są zwykle prostsze do spłaszczenia niż inne typy układów.

Programiści często używają więcej zagnieżdżonych układów niż jest to konieczne. Na przykład kontener RelativeLayout może zawierać pojedynczy element podrzędny, który jest również kontenerem RelativeLayout. Takie zagnieżdżanie jest nadmiarowe i niepotrzebnie zwiększa hierarchię widoków. Lint może zgłosić ten problem, co skróci czas debugowania.

Zastosuj scalanie lub uwzględnianie

Często przyczyną zbędnych zagnieżdżonych układów jest tag <include>. Możesz na przykład zdefiniować układ wielokrotnego użytku w ten sposób:

<LinearLayout>
    <!-- some stuff here -->
</LinearLayout>

Następnie możesz dodać tag <include>, aby dodać do kontenera nadrzędnego ten element:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/app_bg"
    android:gravity="center_horizontal">

    <include layout="@layout/titlebar"/>

    <TextView android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="@string/hello"
              android:padding="10dp" />

    ...

</LinearLayout>

Poprzednie include niepotrzebnie umieszcza pierwszy układ wewnątrz drugiego.

Tag <merge> może pomóc w zapobieganiu temu problemowi. Więcej informacji o tym tagu znajdziesz w artykule Używanie tagu <merge>.

Zastosowanie tańszego układu

Dostosowanie istniejącego schematu układu, aby nie zawierało zbędnych układów, może być niemożliwe. W niektórych przypadkach jedynym rozwiązaniem może być spłaszczenie hierarchii przez przejście na zupełnie inny typ układu.

Może się na przykład okazać, że TableLayout ma te same funkcje co bardziej złożony układ z wieloma zależnościami pozycjonowymi. Biblioteka Jetpacka ConstraintLayout oferuje podobne funkcje do RelativeLayout, a także dodatkowe funkcje ułatwiające tworzenie bardziej przejrzystych i skutecznych układów.