Jak niewielkie zmiany wpływają na płynność i wydajność aplikacji?

8 minut czytania
Komentarze

Cześć, dzisiaj chciałbym Wam pokazać, jakie różnice w działaniu aplikacji mogą powodować pozornie nieduże zmiany. Artykuł nie będzie napisany mocno technicznym językiem, tak więc zachęcam wszystkich do przeczytania. Dla osób niecierpliwych na końcu zebrałem najważniejsze wnioski. Tym samym poprawmy wydajność naszych aplikacji poprzez względnie proste zmiany

Wystarczy kilka zmian, aby wydajność aplikacji się poprawiła

niewielkie zmiany poprawa wydajnosci aplikacji

Test będzie przeprowadzany na następujących urządzeniach:

Test numer 1: LinearLayout vs ConstraintLayout w liście

Zanim przejdziemy dalej, kilka słów objaśnienia. LinearLayout jest kontenerem, układającym elementy widoku w odpowiedniej kolejności, w zależności od wybranej orientacji. Jeśli jest ona pionowa, to widoki układane są jeden po drugim, jeśli pozioma, to jeden obok drugiego. Jeśli chcielibyśmy stworzyć jakieś bardziej zaawansowane struktury, to musimy umieszczać jeden LinearLayout w drugim.

ConstraintLayout rozwiązuje ten problem, ponieważ umożliwia nam tworzenie zaawansowanych widoków, zachowując przy tym płaską strukturę. Oznacza to, że nie musimy już więcej zagnieżdżać jednego layoutu w drugim. Więcej na temat tworzenia interfejsu użytkownika możecie przeczytać w artykule: Jak tworzymy interfejs użytkownika w aplikacjach na Androida?

W naszych pomiarach towarzyszyć będzie aplikacja testowa, którą stworzyłem jakiś czas temu – jej kod możecie znaleźć na moim Githubie. Program ten przypomina nieco serwis społecznościowy. Po uruchomieniu wyświetlany jest RecyclerView zawierający 90 elementów. Na liście występują trzy rodzaje elementów w takiej samej kolejności, jak w galerii powyżej (patrząc od lewej). Kolejność zawsze jest taka sama.

Kody źródłowe wszystkich widoków znajdują się tutaj.

Porównajmy teraz czasy uruchomienia aplikacji dla poszczególnych layoutów. Dane podane w tabelkach są średnią zebraną z trzech następujących po sobie uruchomień.

Całkowity czas potrzebny na uruchomienie:

Note 10+Galaxy S7One M7
ConstraintLayout351 ms772 ms1007 ms
LinearLayout379 ms765 ms926 ms
Dane uzyskane przy pomocy komendy adb shell am start -S -W

Czas potrzebny na zbudowanie samego widoku:

Note 10+Galaxy S7One M7
ConstraintLayout176 ms466 ms657 ms
LinearLayout152 ms416 ms590 ms
Dane uzyskane poprzez porównanie czasu System.currentTimeMillis() z początku metody onCreate oraz po wywołaniu callbacka RecyclerView.post(). Czas początkowy był pobierany po wywołaniu metody super.

W tym zestawieniu moim zdaniem ważniejsza jest druga tabelka, ponieważ pokazuje ona faktyczny czas na zbudowanie naszego widoku. Czasy z pierwszej tabelki zawierają m.in. czas potrzebny na utworzenie procesu, a on jest mocno zależny od aktualnych priorytetów systemu.

Patrząc na powyższe dane, możemy zauważyć, że LinearLayout okazał się wydajniejszy, jeśli chodzi o listę. Jest to poniekąd zgodne z tym, co możemy przeczytać na branżowych forach. Bardzo często spotykałem się z poradami, aby dobrze rozważyć wykorzystanie ConstraintLayout w listach.

Test numer 2 – brak recyklingu widoków w liście

Domyślnie RecyclerView (widok odpowiedzialny za wyświetlanie listy) tworzy tylko tyle widoków, ile zmieści się na ekranie bez potrzeby przewijania (plus lekki margines). W trakcie naszego przewijania w dół nie tworzymy nowych widoków, tylko używamy tych, które zniknęły „u góry” w ramach przewijania. Mechanizm ten można wyłączyć, co spowoduje zbudowanie wszystkich widoków jednocześnie, a w przypadku naszej aplikacji jest ich aż 90.

Zobacz też: Nowy typ anten 5G rozwiąże największy problem tej technologii.

Wbrew pozorom, nie trzeba jakoś mocno się starać, aby doprowadzić do omawianej sytuacji. Wystarczy RecyclerView umieścić w ScrollView. Wiele osób tak robi, żeby dodać dodatkowy, statyczny widok przed lub pod listą ładowaną do RecyclerView.

Całkowity czas potrzebny na uruchomienie:

Note 10+Galaxy S7One M7
ConstraintLayout1632 ms2608 ms4442 ms
LinearLayout1439 ms2116 ms2990 ms
Dane uzyskane przy pomocy komendy adb shell am start -S -W

Czas potrzebny na zbudowanie samego widoku:

Note 10+Galaxy S7One M7
ConstraintLayout1468 ms2158 ms3010 ms
LinearLayout1257 ms1671 ms2178 ms
Dane uzyskane poprzez porównanie czasu System.currentTimeMillis() z początku metody onCreate oraz po wywołaniu callbacka RecyclerView.post(). Czas początkowy był pobierany po wywołaniu metody super.

Tutaj raczej nie powinno być niespodzianek. Wiadomo, że budowanie całej listy naraz będzie sporo dłuższe, niż budowanie tylko kilku widoków. Dlatego, jeśli tworzysz aplikacje na Androida, to unikaj umieszczania RecyclerView w ScrollView. Podobnie jak w pierwszym teście, tutaj też LinearLayout jest górą – różnice te są już bardziej znaczące.

Test numer 3 – duże obrazki

Gdy wyświetlamy obrazki w naszej aplikacji, powinniśmy przycinać ich wielkość, tak aby była dostosowana do wielkości ekranu. Nie powinniśmy wrzucać do ImageView obrazka o szerokości kilku lub kilkunastu tysięcy pikseli, gdy ekran ma szerokość 1080 pikseli. Chyba że chcemy umożliwić zbliżanie.

W ramach przygotowania wymieniłem wszystkie obrazki na takie, które są w rozdzielczości 24 MPx. Spójrzmy najpierw na czas uruchamiania (recykling w RecyclerView jest włączony, widoki są w wersji ConstraintLayout, a largeHeap jest wyłączony).

Wyniki pomiarów są więcej niż zaskakujące

Czas potrzebny na zbudowanie samego widoku:

Note 10+Galaxy S7One M7
Duże obrazki1084 msBrak danychBrak danych
Małe obrazki176 ms466 ms657 ms
Dane uzyskane poprzez porównanie czasu System.currentTimeMillis() z początku metody onCreate oraz po wywołaniu callbacka RecyclerView.post(). Czas początkowy był pobierany po wywołaniu metody super.

Skąd ten „Brak danych” w przypadku Galaxy S7 oraz HTC? Niestety urządzenia te mają za mało pamięci, żeby podołać takiemu wyzwaniu. W przypadku tajwańskiego urządzenia na moment po uruchomieniu nastąpił crash, spowodowany brakiem wystarczającej pamięci (OutOfMemoryException):

niewielkie zmiany poprawa wydajnosci aplikacji

Co możemy się dowiedzieć z tego błędu? Aplikacja zażądała 864 MB RAM w celu wyświetlenia obrazków. Jednak w tym momencie wolnych pozostawało zaledwie 4 MB oraz dodatkowo maksymalna ilość przewidziana dla naszej aplikacji to tylko 189 MB.

niewielkie zmiany poprawa wydajnosci aplikacji
Wykres użycia pamięci RAM z HTC One M7

Popatrzmy jeszcze na wykres pamięci. Widać na nim, że crash następuje praktycznie natychmiast po uruchomieniu aplikacji. Po zakończeniu działania programu pamięć zostaje natychmiast zwolniona

niewielkie zmiany poprawa wydajnosci aplikacji
Wykres użycia pamięci RAM z Samsunga Galaxy S7

Nieco inaczej wyglądało to w przypadku Samsunga Galaxy S7. Początkowo aplikacja z powodzeniem dostawała kolejne przydziały pamięci. Nawet myślałem, że uda się uruchomić aplikację. Możemy powyżej zobaczyć, że program dzielnie walczył przez około 17 sekund, po czym został przerwany. W szczytowym punkcie wykorzystane zostało ponad 2 GB RAM. To więcej niż połowa pamięci operacyjnej dostępnej w urządzeniu. Nieco inaczej wyglądał sam moment zakończenia działania programu. Nie nastąpił żaden crash – po prostu aplikacja została zamknięta. Podejrzewam, że proces został zakończony albo przez linuxowe jądro, albo przez jakiś manager pamięci Samsunga.

Kolejną niespodzianką jest wykres pamięci z Samsunga Galaxy Note 10+:

Wykres użycia pamięci RAM z Samsunga Galaxy Note 10+

Na powyższym wykresie widać, że program potrzebował jedynie 600 MB RAM, żeby wyświetlić aplikacje z dużymi obrazkami. Dopiero przy przewijaniu nastąpił skok do 1 GB. Nie do końca potrafię wytłumaczyć, dlaczego w Note 10+ udało się uruchomić program przy wykorzystaniu mniejszej ilości pamięci, niż w przypadku Galaxy S7. Co prawda w kolejnych wersjach Androida pojawiało się sporo zmian, jeśli chodzi o wczytywanie Bitmap, lecz ostatnia zmiana weszła w Androidzie 8 Oreo, tak więc S7 i Note 10+ powinny działać tutaj identycznie. Jeśli macie swoje typy, napiszcie je śmiało w komentarzu!

Tutaj jeszcze bonus, zobaczcie, jak tragicznie działało przewijanie na Note 10+.

Test numer 4 – statyczny LinearLayout vs ConstraintLayout

W tym teście wykorzystamy widoki z pozostałych testów. Z tą różnicą, że nie wyświetlimy ich za pomocą RecyclerView. Zamiast tego dodamy je statycznie do ScrollView. W odróżnieniu od poprzednich testów, w tym wypadku dodane będą tylko trzy widoki.

Całkowity czas potrzebny na uruchomienie:

Note 10+Galaxy S7One M7
ConstraintLayout573 ms1861 ms2571 ms
LinearLayout348 ms966 ms1003 ms
Dane uzyskane przy pomocy komendy adb shell am start -S -W

Czas potrzebny na zbudowanie samego widoku:

Note 10+Galaxy S7One M7
ConstraintLayout379 ms1180 ms1616 ms
LinearLayout170 ms569 ms603 ms
Dane uzyskane poprzez porównanie czasu System.currentTimeMillis() z początku metody onCreate oraz po wywołaniu callbacka NestedScrollView.post(). Czas początkowy był pobierany po wywołaniu metody super.

Podsumowanie wprowadzania niewielkich zmian w celu uzyskania poprawy wydajności

Z perspektywy programisty można wyciągnąć kilka wniosków, z podziałem na zagadnienia.

LinearLayout kontra ConstraintLayout. W przypadku statycznych widoków testy wykazały, że LinearLayout nadal jest królem, jeśli chodzi o szybkość ładowania widoku. Im plik widoku zawiera więcej elementów, tym większa różnica czasowa jest pomiędzy LinearLayout i ConstraintLayout. Warto o tym pamiętać, jeżeli zależy nam na jak najlepszej optymalizacji.

Listy. Tutaj również LinearLayout wygrywa z ConstraintLayout, jeśli chodzi o wydajność. W przypadku mojej aplikacji różnice wynosiły około 10% z korzyścią dla LinearLayout. Nigdy też nie umieszczaj RecyclerView w ScrollView. W takiej sytuacji Recycler będzie budował wszystkie widoki jednocześnie. Ma to dramatyczny wpływ na wydajność. W moim przypadku aplikacja ładowała się 90% wolniej.

Grafiki. Unikaj wyświetlania dużych grafik. Mój przykład był oczywiście mocno przerysowany i celowo użyłem gigantycznych grafik. Jednak OutOfMemoryException występuje stosunkowo często. Szczególnie przy wyświetlaniu zdjęć wybranych z galerii. Dlatego zawsze pamiętaj o skalowaniu.

Zobacz też: Google dąży do tego, aby RCS stał się w pełni bezpiecznym komunikatorem.

Dzięki za dotrwanie do końca. Jeśli chcecie poznać więcej ciekawostek, to zapraszam do sekcji programowanie oraz na nasze forum. Do zobaczenia już niedługo!

Motyw