Wydajność i hierarchie widoków

Sposób zarządzania hierarchią obiektów View może znacząco wpływać na wydajność aplikacji. Na tej stronie dowiesz się, jak sprawdzić, czy hierarchia widoków spowalnia działanie aplikacji, i poznasz strategie rozwiązywania ewentualnych problemów.

Ta strona skupia się na ulepszaniu układów opartych na View. Informacje na temat zwiększania wydajności Jetpack Compose znajdziesz w artykule na temat wydajności tej usługi.

Skuteczność układu i pomiaru

Potok renderowania obejmuje etap układu i pomiaru, na którym system odpowiednio umieszcza odpowiednie elementy w hierarchii widoków. Część measure na tym etapie określa rozmiary i granice obiektów View. Część dotycząca układu określa, w którym miejscu na ekranie powinny się znajdować obiekty View.

Oba te etapy potoku wiążą się z niewielkim kosztem obejrzenia lub przetwarzanym układem. Zwykle jest to minimalny koszt, który nie wpływa zauważalnie na wydajność. Ten limit może jednak być większy, gdy aplikacja doda lub usunie obiekty View, na przykład gdy obiekt RecyclerView je recyklingowo lub ponownie. Koszt może być też wyższy, jeśli trzeba zmienić rozmiar obiektu View tak, aby spełniał swoje ograniczenia. Jeśli na przykład aplikacja wywołuje metodę SetText() na obiekcie View, który zawija tekst, View może wymagać zmiany rozmiaru.

Zbyt długi czas może uniemożliwić wyrenderowanie klatki w ciągu 16 ms, co może doprowadzić do zmniejszenia klatek i zakłóceń animacji.

Nie można przenieść tych operacji do wątku roboczego, a aplikacja musi je przetwarzać w wątku głównym. Dlatego najlepiej je optymalizować, aby zajmowały jak najmniej czasu.

Zarządzanie złożonymi układami

Układy Androida umożliwiają zagnieżdżanie obiektów UI w hierarchii widoków. Takie zagnieżdżanie może też wiązać się z kosztami układu. Gdy aplikacja przetwarza obiekt na potrzeby układu, wykonuje ten sam proces na wszystkich elementach podrzędnych układu.

W przypadku skomplikowanego układu koszt może pojawić się tylko wtedy, gdy system pierwszy raz obliczy układ. Jeśli na przykład aplikacja odświeża złożony element listy w obiekcie RecyclerView, system musi rozmieścić wszystkie obiekty. W innym przykładzie drobne zmiany mogą rozprzestrzeniać się w górę łańcucha w kierunku elementu nadrzędnego, aż dotrą do obiektu, który nie wpływa na rozmiar elementu nadrzędnego.

Typowym powodem, dla którego układ zajmuje dużo czasu, jest zagnieżdżanie w innych hierarchiach obiektów View. Każdy zagnieżdżony obiekt układu zwiększa koszty etapu układu. Im bardziej płaska hierarchia, tym mniej czasu zajmie ukończenie etapu układu.

Zalecamy użycie edytora układu do utworzenia elementu ConstraintLayout zamiast RelativeLayout lub LinearLayout, ponieważ zwiększa to wydajność i zmniejsza zagnieżdżanie układów. Jednak w przypadku prostych układów, które można osiągnąć za pomocą właściwości FrameLayout, zalecamy użycie właściwości FrameLayout.

Jeśli używasz klasy RelativeLayout, możesz uzyskać ten sam efekt przy niższych kosztach, używając zamiast tego 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 on wielu przebiegów układu, co wyjaśniono w następnej sekcji.

Zalecamy też użycie atrybutu RecyclerView zamiast ListView, ponieważ pozwala on wielokrotnie stosować układy poszczególnych elementów listy, co jest wydajniejsze i może zwiększyć wydajność przewijania.

Podwójne opodatkowanie

Zwykle platforma wykonuje układ lub etap pomiaru w ramach jednego przebiegu. Jednak w niektórych skomplikowanych przypadkach układu platforma może wielokrotnie próbować wielokrotnie w niektórych częściach hierarchii, które wymagają wykonania wielu uruchomień przed ostatecznym umiejscowieniem elementów. Konieczność wykonania więcej niż jednej iteracji układu i pomiaru jest nazywana podwójnym opodatkowaniem.

Jeśli na przykład korzystasz z kontenera RelativeLayout, który umożliwia umieszczanie obiektów View w odniesieniu do pozycji innych obiektów View, platforma wykonuje taką sekwencję:

  1. Wykonuje przekazywanie układu i pomiaru, podczas którego platforma oblicza pozycję i rozmiar każdego obiektu podrzędnego na podstawie żądania każdego obiektu podrzędnego.
  2. Używa tych danych z uwzględnieniem wag obiektów do ustalania właściwego położenia skorelowanych widoków.
  3. Wykonuje drugie przekazanie układu, by zakończyć umiejscowienie obiektów.
  4. Przechodzi do następnego etapu procesu renderowania.

Im większa hierarchia widoków danych, tym większe ryzyko spadku skuteczności.

Jak już wspomnieliśmy, ConstraintLayout jest zwykle bardziej wydajny niż inne układy z wyjątkiem FrameLayout. Jest mniej podatny na wielokrotne karty układu, a w wielu przypadkach eliminuje potrzebę zagnieżdżania układów.

Kontenery inne niż RelativeLayout również mogą zwiększyć podwójne opodatkowanie. Przykład:

  • Widok LinearLayout może spowodować podwójne podanie układu i pomiaru, jeśli ustawisz go w poziomie. Podwójne przekazywanie układu i pomiarów może też wystąpić w orientacji pionowej, jeśli dodasz measureWithLargestChild. W takim przypadku platforma może wymagać drugiego przekazania, aby uzyskać prawidłowe rozmiary obiektów.
  • Obiekt GridLayout umożliwia też pozycjonowanie względne, ale zwykle pozwala uniknąć podwójnego opodatkowania, ponieważ wstępnie przetwarza relacje pozycjonujące między widokami podrzędnymi. Jeśli jednak układ używa wag lub wypełnienia klasą Gravity, tracisz korzyści z wstępnego przetwarzania, a platforma może wymagać wykonania wielu kart, jeśli kontenerem jest RelativeLayout.

Wielokrotne podanie układu i pomiaru niekoniecznie zwiększa wydajność. Jednak w niewłaściwym miejscu mogą być uciążliwe. Zachowaj ostrożność w sytuacjach, w których do kontenera ma zastosowanie jeden z tych warunków:

  • To główny element Twojej hierarchii widoków.
  • Pod spodem kryje się dogłębna hierarchia widoków.
  • Może on wypełniać ekran w wielu przypadkach, podobnie jak elementy podrzędne w obiekcie ListView.

Diagnozowanie problemów z hierarchią widoków

Wydajność układu to skomplikowany problem obejmujący wiele aspektów. Opisane poniżej narzędzia pomogą Ci zidentyfikować miejsca, w których występują wąskie gardła wydajności. Niektóre narzędzia podają mniej precyzyjne informacje, ale mogą też zawierać przydatne wskazówki.

Perfetto

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

Profil renderowania GPU

Dostępne na urządzeniu narzędzie Renderowanie GPU w profilu na urządzeniach z Androidem 6.0 (poziom interfejsu API 23) i nowszym może dostarczyć konkretnych informacji o wąskich gardłach wydajności. To narzędzie pozwala sprawdzić, jak długo trwa etap ustalania układu i pomiaru każdej klatki renderowania. Dane te mogą pomóc w diagnozowaniu problemów z wydajnością środowiska wykonawczego i ustaleniu, jakie problemy z układem i pomiarami trzeba rozwiązać.

W ramach graficznej reprezentacji zarejestrowanych danych funkcja renderowania GPU profilu wykorzystuje kolor niebieski, aby odzwierciedlić czas układu. Więcej informacji o korzystaniu z tego narzędzia znajdziesz w artykule Profilowanie szybkości renderowania GPU.

Liniowa

Narzędzie Lint w Android Studio pomaga zorientować się w niezgodnościach w hierarchii widoków. Aby użyć tego narzędzia, wybierz Analiza > Sprawdź kod, jak pokazano na ilustracji 1.

Rysunek 1. Kliknij Zbadaj kod w Android Studio.

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 rozwiniętych informacji.

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

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

Więcej informacji na temat konkretnych tematów i problemów na tym obszarze znajdziesz w dokumentacji Lint.

Inspektor układu

Narzędzie Inspektor układu w Android Studio pozwala obrazowo przedstawić hierarchię widoków aplikacji. To dobry sposób na poruszanie się po hierarchii aplikacji, zapewniając przejrzystą wizualną reprezentację łańcucha nadrzędnego danego widoku i sprawdzanie układów tworzonych przez aplikację.

Widoki dostępne w inspektorze układu również pomagają w identyfikacji problemów ze skutecznością wynikających z podwójnych podatków. Może też być sposobem na identyfikowanie głębokich łańcuchów zagnieżdżonych układów lub obszarów z dużą liczbą zagnieżdżonych elementów podrzędnych, co może być źródłem kosztów wydajności. W takich przypadkach etapy układu i pomiaru mogą być kosztowne i powodować problemy z wydajnością.

Więcej informacji znajdziesz w artykule Debugowanie układu przy użyciu Inspektora układu i weryfikacji układu.

Rozwiąż problemy z hierarchią widoków

Podstawowa koncepcja rozwiązywania problemów z wydajnością wynikających z hierarchii widoków może być trudna w praktyce. Aby hierarchie widoków danych nie nakładały kar za skuteczność, polegają na spłaszczeniu hierarchii widoków i ograniczeniu podwójnego opodatkowania. W tej sekcji omawiamy strategie 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. Zmniejsza to potrzebę zagnieżdżania jednego obiektu ConstaintLayout i pomaga spłaszczyć hierarchię widoków. Zwykle łatwiej jest spłaszczyć hierarchie za pomocą parametru ConstraintLayout w porównaniu z innymi typami układów.

Deweloperzy często używają więcej zagnieżdżonych układów, niż jest to konieczne. Na przykład kontener RelativeLayout może zawierać jeden element podrzędny, który jest też kontenerem RelativeLayout. Takie zagnieżdżanie jest niepotrzebne i zwiększa hierarchię widoków danych. Lint może zgłosić ten problem, skracając czas debugowania.

Zastosuj scalenie lub uwzględnienie

Częstą przyczyną nadmiarowych układów zagnieżdżonych jest tag <include>. Układ do wielokrotnego użytku możesz np. zdefiniować w ten sposób:

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

Następnie możesz dodać tag <include>, by dodać do kontenera nadrzędnego następujący 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 z nich niepotrzebnie zagnieżdżają pierwszy układ w drugim.

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

Wybierz tańszy układ

Dostosowanie istniejącego schematu układu w taki sposób, aby nie zawierał zbędnych układów, może nie być możliwe. W niektórych przypadkach jedynym rozwiązaniem może być spłaszczenie hierarchii przez przełączenie się na zupełnie inny typ układu.

Może się np. okazać, że funkcja TableLayout oferuje te same funkcje co bardziej złożony układ z wieloma zależnościami pozycjonowania. Biblioteka Jetpack ConstraintLayout ma podobną funkcjonalność do RelativeLayout i zawiera więcej funkcji, które pomagają tworzyć bardziej płaskie i wydajne układy.