Wskazówki i narzędzia do optymalizacji aplikacji na Androida
Opublikowany: 2022-03-11Urządzenia z Androidem mają wiele rdzeni, więc pisanie płynnych aplikacji to proste zadanie dla każdego, prawda? Zło. Ponieważ wszystko w Androidzie można zrobić na wiele różnych sposobów, wybór najlepszej opcji może być trudny. Jeśli chcesz wybrać najskuteczniejszą metodę, musisz wiedzieć, co dzieje się pod maską. Na szczęście nie musisz polegać na swoich uczuciach ani węchu, ponieważ istnieje wiele narzędzi, które mogą pomóc Ci znaleźć wąskie gardła, mierząc i opisywać, co się dzieje. Odpowiednio zoptymalizowane i płynnie działające aplikacje znacznie poprawiają wrażenia użytkownika, a także zużywają mniej baterii.
Zobaczmy najpierw kilka liczb, aby zastanowić się, jak ważna jest optymalizacja. Według postu Nimbledroid, 86% użytkowników (w tym ja) odinstalowało aplikacje po użyciu ich tylko raz z powodu słabej wydajności. Jeśli ładujesz jakąś treść, masz mniej niż 11 sekund na pokazanie jej użytkownikowi. Tylko co trzeci użytkownik da Ci więcej czasu. Z tego powodu możesz też otrzymać wiele złych recenzji w Google Play.
Pierwszą rzeczą, jaką każdy użytkownik zauważa raz po raz, jest czas uruchamiania aplikacji. Według innego postu Nimbledroid, ze 100 najlepszych aplikacji, 40 zaczyna się w mniej niż 2 sekundy, a 70 zaczyna w mniej niż 3 sekundy. Więc jeśli to możliwe, powinieneś generalnie wyświetlać niektóre treści tak szybko, jak to możliwe i trochę opóźnić sprawdzanie i aktualizacje w tle.
Zawsze pamiętaj, przedwczesna optymalizacja jest źródłem wszelkiego zła. Nie powinieneś też marnować zbyt dużo czasu na mikrooptymalizację. Zobaczysz największe korzyści z optymalizacji kodu, który jest często uruchamiany. Na przykład obejmuje to funkcję onDraw()
, która uruchamia każdą klatkę, najlepiej 60 razy na sekundę. Rysowanie jest najwolniejszą operacją, więc spróbuj przerysować tylko to, co musisz. Więcej na ten temat pojawi się później.
Wskazówki dotyczące wydajności
Dość teorii, oto lista niektórych rzeczy, które powinieneś rozważyć, jeśli wydajność ma dla Ciebie znaczenie.
1. String a StringBuilder
Załóżmy, że masz String iz jakiegoś powodu chcesz dodać do niego więcej Stringów 10 tysięcy razy. Kod mógłby wyglądać mniej więcej tak.
String string = "hello"; for (int i = 0; i < 10000; i++) { string += " world"; }
Możesz zobaczyć na monitorach Android Studio, jak nieefektywne może być łączenie ciągów. Jest mnóstwo kolekcji śmieci (GC).
Ta operacja trwa około 8 sekund na moim całkiem niezłym urządzeniu z Androidem 5.1.1. Bardziej wydajnym sposobem osiągnięcia tego samego celu jest użycie StringBuildera, takiego jak ten.
StringBuilder sb = new StringBuilder("hello"); for (int i = 0; i < 10000; i++) { sb.append(" world"); } String string = sb.toString();
Na tym samym urządzeniu dzieje się to niemal natychmiast, w mniej niż 5 ms. Wizualizacje procesora i pamięci są prawie całkowicie płaskie, więc możesz sobie wyobrazić, jak duża jest ta poprawa. Zauważ jednak, że aby osiągnąć tę różnicę, musieliśmy dołączyć 10 tysięcy Stringów, czego prawdopodobnie nie robisz często. Więc jeśli dodasz tylko kilka ciągów raz, nie zobaczysz żadnej poprawy. Przy okazji, jeśli to zrobisz:
String string = "hello" + " world";
Jest wewnętrznie konwertowany na StringBuilder, więc będzie działał dobrze.
Możesz się zastanawiać, dlaczego łączenie ciągów w pierwszym sposobie jest tak wolne? Wynika to z faktu, że Stringi są niezmienne, więc raz utworzone nie można ich zmienić. Nawet jeśli myślisz, że zmieniasz wartość ciągu, w rzeczywistości tworzysz nowy ciąg z nową wartością. W przykładzie takim jak:
String myString = "hello"; myString += " world";
To, co dostaniesz w pamięci, to nie 1 String „hello world”, ale w rzeczywistości 2 Strings. String myString będzie zawierać „hello world”, jak można się spodziewać. Jednak oryginalny String z wartością „hello” wciąż żyje, bez żadnego odniesienia do niego, czekając na zebranie śmieci. Jest to również powód, dla którego powinieneś przechowywać hasła w tablicy znaków zamiast w ciągu. Jeśli przechowujesz hasło jako ciąg, pozostanie ono w pamięci w formacie czytelnym dla człowieka do następnego GC przez nieprzewidywalny czas. Wracając do niezmienności opisanej powyżej, String pozostanie w pamięci, nawet jeśli po użyciu przypiszesz mu inną wartość. Jeśli jednak opróżnisz tablicę znaków po użyciu hasła, zniknie ona zewsząd.
2. Wybór właściwego typu danych
Zanim zaczniesz pisać kod, powinieneś zdecydować, jakich typów danych użyjesz w swojej kolekcji. Na przykład, czy powinieneś użyć Vector
czy ArrayList
? Cóż, to zależy od twojego przypadku użycia. Jeśli potrzebujesz kolekcji bezpiecznej dla wątków, która pozwoli na pracę tylko jednego wątku na raz, powinieneś wybrać Vector
, ponieważ jest zsynchronizowany. W innych przypadkach prawdopodobnie powinieneś trzymać się ArrayList
, chyba że naprawdę masz konkretny powód do używania wektorów.
A co z przypadkiem, gdy chcesz mieć kolekcję z unikalnymi przedmiotami? Cóż, prawdopodobnie powinieneś wybrać Set
. Nie mogą zawierać duplikatów z założenia, więc nie będziesz musiał o nie dbać sam. Istnieje wiele rodzajów zestawów, więc wybierz taki, który pasuje do Twojego przypadku użycia. W przypadku prostej grupy unikalnych elementów możesz użyć HashSet
. Jeśli chcesz zachować kolejność elementów, w których zostały wstawione, wybierz LinkedHashSet
. TreeSet
sortuje elementy automatycznie, więc nie będziesz musiał wywoływać na nim żadnych metod sortowania. Powinno również efektywnie sortować elementy, bez konieczności myślenia o algorytmach sortowania.
— 5 zasad programowania Roba Pike’a
Sortowanie liczb całkowitych lub łańcuchów jest dość proste. A co, jeśli chcesz posortować klasę według jakiejś właściwości? Załóżmy, że piszesz listę spożywanych posiłków i przechowujesz ich nazwy oraz sygnatury czasowe. Jak posortowałbyś posiłki według sygnatury czasowej od najniższej do najwyższej? Na szczęście to całkiem proste. Wystarczy zaimplementować interfejs Comparable
w klasie Meal
i zastąpić funkcję compareTo()
. Aby posortować posiłki od najniższego znacznika czasu do najwyższego, moglibyśmy napisać coś takiego.
@Override public int compareTo(Object object) { Meal meal = (Meal) object; if (this.timestamp < meal.getTimestamp()) { return -1; } else if (this.timestamp > meal.getTimestamp()) { return 1; } return 0; }
3. Aktualizacje lokalizacji
Istnieje wiele aplikacji, które zbierają lokalizację użytkownika. W tym celu należy użyć interfejsu Google Location Services API, który zawiera wiele przydatnych funkcji. O korzystaniu z niego jest osobny artykuł, więc nie będę tego powtarzał.
Chciałbym tylko podkreślić kilka ważnych punktów z perspektywy wydajności.
Przede wszystkim używaj tylko najbardziej dokładnej lokalizacji, której potrzebujesz. Na przykład, jeśli prognozujesz pogodę, nie potrzebujesz najdokładniejszej lokalizacji. Uzyskanie tylko bardzo nierównego obszaru w oparciu o sieć jest szybsze i bardziej energooszczędne. Możesz to osiągnąć, ustawiając priorytet na LocationRequest.PRIORITY_LOW_POWER
.
Możesz także użyć funkcji LocationRequest
o nazwie setSmallestDisplacement()
. Ustawienie tego w metrach spowoduje, że Twoja aplikacja nie będzie powiadamiana o zmianie lokalizacji, jeśli była mniejsza niż podana wartość. Na przykład, jeśli masz mapę z pobliskimi restauracjami w pobliżu i ustawisz najmniejsze przemieszczenie na 20 metrów, aplikacja nie będzie wysyłać próśb o sprawdzenie restauracji, jeśli użytkownik po prostu chodzi po pokoju. Prośby byłyby bezużyteczne, ponieważ i tak nie byłoby żadnej nowej restauracji w pobliżu.
Druga zasada to żądanie aktualizacji lokalizacji tylko tak często, jak tego potrzebujesz. To dość oczywiste. Jeśli naprawdę budujesz tę aplikację prognozy pogody, nie musisz pytać o lokalizację co kilka sekund, ponieważ prawdopodobnie nie masz tak dokładnych prognoz (skontaktuj się ze mną, jeśli masz). Możesz użyć funkcji setInterval()
, aby ustawić wymagany interwał, w którym urządzenie będzie aktualizować Twoją aplikację o lokalizacji. Jeśli wiele aplikacji będzie nadal żądać lokalizacji użytkownika, każda aplikacja zostanie powiadomiona o każdej nowej aktualizacji lokalizacji, nawet jeśli masz wyższy zestaw setInterval()
. Aby Twoja aplikacja nie była zbyt często powiadamiana, pamiętaj, aby zawsze ustawiać najszybszy interwał aktualizacji za pomocą setFastestInterval()
.
I wreszcie trzecia zasada to żądanie aktualizacji lokalizacji tylko wtedy, gdy ich potrzebujesz. Jeśli wyświetlasz jakieś pobliskie obiekty na mapie co x sekund, a aplikacja działa w tle, nie musisz znać nowej lokalizacji. Nie ma powodu, aby aktualizować mapę, jeśli użytkownik i tak jej nie widzi. Pamiętaj, aby w razie potrzeby przestać nasłuchiwać aktualizacji lokalizacji, najlepiej w onPause()
. Następnie możesz wznowić aktualizacje w onResume()
.
4. Żądania sieciowe
Istnieje duże prawdopodobieństwo, że Twoja aplikacja korzysta z Internetu do pobierania lub przesyłania danych. Jeśli tak, masz kilka powodów, aby zwracać uwagę na obsługę żądań sieciowych. Jednym z nich są dane mobilne, które są bardzo ograniczone do wielu osób i nie należy ich marnować.
Drugi to bateria. Zarówno Wi-Fi, jak i sieci komórkowe mogą zużywać jej sporo, jeśli są używane zbyt często. Powiedzmy, że chcesz pobrać 1 kb. Aby wysłać żądanie do sieci, musisz obudzić radio komórkowe lub WiFi, a następnie możesz pobrać swoje dane. Jednak radio nie zaśnie od razu po operacji. Pozostanie w dość aktywnym stanie przez około 20-40 sekund, w zależności od urządzenia i operatora.
Więc co możesz z tym zrobić? Seria. Aby uniknąć wybudzania radia co kilka sekund, pobierz z wyprzedzeniem rzeczy, których użytkownik może potrzebować w nadchodzących minutach. Właściwy sposób grupowania jest bardzo dynamiczny w zależności od Twojej aplikacji, ale jeśli to możliwe, powinieneś pobrać dane, których użytkownik może potrzebować w ciągu najbliższych 3-4 minut. Można również edytować parametry wsadu w zależności od typu internetu użytkownika lub stanu ładowania. Na przykład, jeśli użytkownik korzysta z Wi-Fi podczas ładowania, możesz wstępnie pobrać znacznie więcej danych niż w przypadku korzystania z mobilnego Internetu z niskim poziomem naładowania baterii. Wzięcie pod uwagę wszystkich tych zmiennych może być trudne, co zrobiłoby niewiele osób. Na szczęście na ratunek jest GCM Network Manager!
GCM Network Manager to naprawdę pomocna klasa z wieloma konfigurowalnymi atrybutami. Możesz łatwo zaplanować zarówno powtarzające się, jak i jednorazowe zadania. Przy powtarzających się zadaniach możesz ustawić najniższy, a także najwyższy interwał powtórzeń. Umożliwi to grupowanie nie tylko żądań, ale także żądań z innych aplikacji. Radio musi być wybudzane tylko raz na pewien czas, a gdy jest włączone, wszystkie aplikacje w kolejce pobierają i wgrywają to, co mają. Ten menedżer jest również świadomy typu sieci urządzenia i stanu ładowania, dzięki czemu można odpowiednio dostosować. Więcej szczegółów i próbek znajdziecie w tym artykule, zachęcam do sprawdzenia. Przykładowe zadanie wygląda tak:
Task task = new OneoffTask.Builder() .setService(CustomService.class) .setExecutionWindow(0, 30) .setTag(LogService.TAG_TASK_ONEOFF_LOG) .setUpdateCurrent(false) .setRequiredNetwork(Task.NETWORK_STATE_CONNECTED) .setRequiresCharging(false) .build();
Przy okazji, od wersji Androida 3.0, jeśli wykonasz żądanie sieciowe w głównym wątku, otrzymasz NetworkOnMainThreadException
. To z pewnością ostrzeże cię, abyś nie robił tego ponownie.
5. Odbicie
Refleksja to zdolność klas i obiektów do sprawdzania własnych konstruktorów, pól, metod i tak dalej. Jest używany zwykle w celu zapewnienia kompatybilności wstecznej, aby sprawdzić, czy dana metoda jest dostępna dla określonej wersji systemu operacyjnego. Jeśli musisz w tym celu użyć odbicia, pamiętaj o buforowaniu odpowiedzi, ponieważ użycie odbicia jest dość powolne. Niektóre powszechnie używane biblioteki również używają Reflection, na przykład Roboguice do wstrzykiwania zależności. To jest powód, dla którego powinieneś preferować Dagger 2. Więcej informacji na temat odbicia znajdziesz w osobnym poście.
6. Autoboks
Automatyczne pakowanie i rozpakowywanie to procesy konwersji typu pierwotnego na typ Object lub odwrotnie. W praktyce oznacza to zamianę liczby całkowitej na liczbę całkowitą. W tym celu kompilator używa wewnętrznie funkcji Integer.valueOf()
. Konwersja jest nie tylko powolna, obiekty zajmują również dużo więcej pamięci niż ich prymitywne odpowiedniki. Spójrzmy na kod.
Integer total = 0; for (int i = 0; i < 1000000; i++) { total += i; }
Chociaż zajmuje to średnio 500 ms, przepisanie go, aby uniknąć autoboxingu, drastycznie go przyspieszy.
int total = 0; for (int i = 0; i < 1000000; i++) { total += i; }
To rozwiązanie działa z prędkością około 2 ms, czyli 25 razy szybciej. Jeśli mi nie wierzysz, przetestuj to. Liczby będą oczywiście różne w zależności od urządzenia, ale nadal powinno być znacznie szybciej. To także bardzo prosty krok do optymalizacji.
Okej, prawdopodobnie nie tworzysz często takiej zmiennej typu Integer. Ale co z przypadkami, w których trudniej tego uniknąć? Jak na mapie, gdzie musisz użyć Objects, jak Map<Integer, Integer>
? Spójrz na rozwiązanie, z którego korzysta wiele osób.
Map<Integer, Integer> myMap = new HashMap<>(); for (int i = 0; i < 100000; i++) { myMap.put(i, random.nextInt()); }
Wstawienie 100k losowych intów do mapy zajmuje około 250 ms. Teraz spójrz na rozwiązanie z SparseIntArray.
SparseIntArray myArray = new SparseIntArray(); for (int i = 0; i < 100000; i++) { myArray.put(i, random.nextInt()); }
Zajmuje to o wiele mniej, około 50 ms. Jest to również jedna z łatwiejszych metod poprawy wydajności, ponieważ nie trzeba robić nic skomplikowanego, a kod pozostaje czytelny. Podczas gdy uruchomienie przejrzystej aplikacji z pierwszym rozwiązaniem zajęło mi 13 MB pamięci, użycie prymitywnych int zajęło mniej niż 7 MB, a więc tylko połowę.
SparseIntArray to tylko jedna z fajnych kolekcji, które pomogą Ci uniknąć autoboxingu. Mapę taką jak Map<Integer, Long>
można zastąpić przez SparseLongArray
, ponieważ wartość mapy jest typu Long
. Jeśli spojrzysz na kod źródłowy SparseLongArray
, zobaczysz coś całkiem interesującego. Pod maską jest to w zasadzie tylko para tablic. W podobny sposób można również użyć SparseBooleanArray
.
Jeśli czytasz kod źródłowy, być może zauważyłeś notatkę mówiącą, że SparseIntArray
może być wolniejszy niż HashMap
. Dużo eksperymentowałem, ale dla mnie SparseIntArray
zawsze był lepszy zarówno pod względem pamięci, jak i wydajności. Sądzę, że nadal od Ciebie zależy, który wybierzesz, poeksperymentuj ze swoimi przypadkami użycia i zobacz, który najbardziej Ci odpowiada. Zdecydowanie miej w głowie SparseArrays
podczas korzystania z map.
7. On Draw
Jak wspomniałem powyżej, gdy optymalizujesz wydajność, prawdopodobnie największe korzyści odniesiesz w optymalizacji kodu, który często się uruchamia. Jedną z często uruchamianych funkcji jest onDraw()
. Może cię nie dziwić, że odpowiada za rysowanie widoków na ekranie. Ponieważ urządzenia zwykle działają z prędkością 60 fps, funkcja jest uruchamiana 60 razy na sekundę. Każda klatka ma 16 ms do pełnej obsługi, łącznie z jej przygotowaniem i rysowaniem, więc naprawdę powinieneś unikać powolnych funkcji. Tylko główny wątek może rysować na ekranie, dlatego należy unikać wykonywania na nim kosztownych operacji. Jeśli zamrozisz główny wątek na kilka sekund, może pojawić się niesławne okno dialogowe Application Not Responding (ANR). Do zmiany rozmiaru obrazów, pracy z bazą danych itp. użyj wątku tła.
Widziałem ludzi, którzy próbowali skrócić swój kod, myśląc, że w ten sposób będzie to bardziej efektywne. To zdecydowanie nie jest droga, ponieważ krótszy kod całkowicie nie oznacza szybszego kodu. W żadnym wypadku nie należy mierzyć jakości kodu liczbą wierszy.

Jedną z rzeczy, których należy unikać w onDraw()
, jest przydzielanie obiektów takich jak Paint. Przygotuj wszystko w konstruktorze, aby było gotowe do rysowania. Nawet jeśli zoptymalizowałeś onDraw()
, powinieneś wywoływać ją tylko tak często, jak to konieczne. Co jest lepsze niż wywołanie zoptymalizowanej funkcji? Cóż, w ogóle nie wywołując żadnej funkcji. Jeśli chcesz narysować tekst, istnieje całkiem zgrabna funkcja pomocnicza o nazwie drawText()
, w której możesz określić takie rzeczy, jak tekst, współrzędne i kolor tekstu.
8. Posiadacze widoku
Prawdopodobnie znasz ten, ale nie mogę go pominąć. Wzorzec projektowy Viewholder to sposób na płynniejsze przewijanie list. Jest to rodzaj buforowania widoków, który może poważnie zredukować wywołania findViewById()
i zawyżanie widoków poprzez ich przechowywanie. To może wyglądać mniej więcej tak.
static class ViewHolder { TextView title; TextView text; public ViewHolder(View view) { title = (TextView) view.findViewById(R.id.title); text = (TextView) view.findViewById(R.id.text); } }
Następnie, wewnątrz funkcji getView()
twojego adaptera, możesz sprawdzić, czy masz użyteczny widok. Jeśli nie, tworzysz jeden.
ViewHolder viewHolder; if (convertView == null) { convertView = inflater.inflate(R.layout.list_item, viewGroup, false); viewHolder = new ViewHolder(convertView); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.title.setText("Hello World");
W Internecie można znaleźć wiele przydatnych informacji o tym wzorze. Może być również używany w przypadkach, gdy widok listy zawiera wiele różnych typów elementów, takich jak niektóre nagłówki sekcji.
9. Zmiana rozmiaru obrazów
Możliwe, że Twoja aplikacja będzie zawierać obrazy. Jeśli pobierasz pliki JPG z Internetu, mogą one mieć naprawdę ogromne rozdzielczości. Jednak urządzenia, na których będą wyświetlane, będą znacznie mniejsze. Nawet jeśli zrobisz zdjęcie aparatem swojego urządzenia, przed wyświetleniem należy je zmniejszyć, ponieważ rozdzielczość zdjęcia jest znacznie większa niż rozdzielczość wyświetlacza. Zmiana rozmiaru obrazów przed ich wyświetleniem jest kluczowa. Gdybyś spróbował wyświetlić je w pełnej rozdzielczości, dość szybko zabrakłoby Ci pamięci. O sprawnym wyświetlaniu bitmap w dokumentach Androida napisano wiele, spróbuję to podsumować.
Więc masz bitmapę, ale nic o niej nie wiesz. W Twojej usłudze znajduje się przydatna flaga bitmap o nazwie inJustDecodeBounds, która umożliwia sprawdzenie rozdzielczości bitmapy. Załóżmy, że twoja bitmapa ma rozmiar 1024x768, a ImageView użyty do jej wyświetlenia ma tylko 400x300. Powinieneś dalej dzielić rozdzielczość bitmapy przez 2, dopóki nie będzie ona większa niż podany ImageView. Jeśli to zrobisz, zmniejszy próbkowanie mapy bitowej o współczynnik 2, co daje bitmapę o wymiarach 512x384. Bitmapa z downsamplingiem zużywa 4x mniej pamięci, co bardzo pomoże ci uniknąć słynnego błędu OutOfMemory.
Teraz, kiedy już wiesz, jak to zrobić, nie powinieneś tego robić. … Przynajmniej nie, jeśli Twoja aplikacja w dużym stopniu opiera się na obrazach i nie jest to tylko 1-2 obrazy. Zdecydowanie unikaj takich rzeczy, jak ręczne zmienianie rozmiaru i recykling obrazów, użyj do tego bibliotek innych firm. Najpopularniejsze to Picasso by Square, Universal Image Loader, Fresco by Facebook czy mój ulubiony Glide. Wokół niego jest ogromna aktywna społeczność programistów, więc w sekcji problemów na GitHubie można znaleźć wiele pomocnych osób.
10. Tryb ścisły
Tryb ścisły to całkiem przydatne narzędzie programistyczne, o którym wiele osób nie wie. Jest zwykle używany do wykrywania żądań sieciowych lub dostępu do dysku z głównego wątku. Możesz ustawić, jakich problemów powinien szukać w trybie ścisłym i jaką karę powinien wywołać. Przykład google wygląda tak:
public void onCreate() { if (DEVELOPER_MODE) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .penaltyLog() .penaltyDeath() .build()); } super.onCreate(); }
Jeśli chcesz wykryć każdy problem, który może znaleźć tryb ścisły, możesz również użyć detectAll()
. Podobnie jak w przypadku wielu wskazówek dotyczących wydajności, nie powinieneś ślepo próbować naprawiać wszystkich raportów w trybie ścisłym. Po prostu to zbadaj, a jeśli masz pewność, że to nie problem, zostaw to w spokoju. Upewnij się również, że używasz trybu ścisłego tylko do debugowania i zawsze wyłączaj go w kompilacjach produkcyjnych.
Wydajność debugowania: sposób zawodowy
Zobaczmy teraz kilka narzędzi, które mogą pomóc Ci znaleźć wąskie gardła lub przynajmniej pokazać, że coś jest nie tak.
1. Monitor Android
To jest narzędzie wbudowane w Android Studio. Domyślnie monitor Androida znajduje się w lewym dolnym rogu i można tam przełączać się między 2 zakładkami. Logcat i monitory. Sekcja Monitory zawiera 4 różne wykresy. Sieć, procesor, GPU i pamięć. Są dość wymowne, więc szybko je przejdę. Oto zrzut ekranu wykresów wykonanych podczas analizowania niektórych plików JSON podczas ich pobierania.
Część Network pokazuje ruch przychodzący i wychodzący w KB/s. Część CPU wyświetla wykorzystanie procesora w procentach. Monitor GPU wyświetla, ile czasu zajmuje renderowanie ramek okna interfejsu użytkownika. Jest to najbardziej szczegółowy monitor z tych 4, więc jeśli chcesz uzyskać więcej informacji na jego temat, przeczytaj to.
Wreszcie mamy monitor pamięci, z którego prawdopodobnie będziesz korzystać najczęściej. Domyślnie pokazuje aktualną ilość wolnej i przydzielonej pamięci. Możesz również wymusić zbieranie śmieci, aby sprawdzić, czy ilość używanej pamięci spada. Ma przydatną funkcję o nazwie Dump Java Heap, która utworzy plik HPROF, który można otworzyć za pomocą przeglądarki i analizatora HPROF. To pozwoli ci zobaczyć, ile obiektów przydzieliłeś, ile pamięci jest zajęte przez co i być może które obiekty powodują wycieki pamięci. Nauka obsługi tego analizatora nie jest najprostszym zadaniem, ale warto. Następną rzeczą, którą możesz zrobić z Monitorem pamięci, jest śledzenie alokacji w czasie, które możesz uruchamiać i zatrzymywać, jak chcesz. Może się przydać w wielu przypadkach, na przykład podczas przewijania lub obracania urządzenia.
2. Przerysowanie GPU
Jest to proste narzędzie pomocnicze, które możesz aktywować w Opcjach programisty po włączeniu trybu programisty. Wybierz Debuguj przerysowanie GPU, „Pokaż obszary przerysowania”, a na ekranie pojawią się dziwne kolory. W porządku, tak ma się stać. Kolory oznaczają, ile razy dany obszar został przerysowany. Prawdziwy kolor oznacza, że nie było przerysowania, do tego należy dążyć. Niebieski oznacza jedno przeciągnięcie, zielony oznacza dwa, różowy trzy, czerwony cztery.
Chociaż najlepiej jest widzieć prawdziwy kolor, zawsze zobaczysz pewne przerysowania, zwłaszcza wokół tekstów, szuflad nawigacyjnych, okien dialogowych i innych. Więc nie próbuj całkowicie się go pozbyć. Jeśli Twoja aplikacja jest niebieskawa lub zielonkawa, to prawdopodobnie w porządku. Jeśli jednak widzisz zbyt dużo czerwieni na niektórych prostych ekranach, powinieneś zbadać, co się dzieje. Może to być zbyt wiele fragmentów ułożonych jeden na drugim, jeśli będziesz je dodawać zamiast zastępować. Jak wspomniałem powyżej, rysowanie jest najwolniejszą częścią aplikacji, więc nie ma sensu rysować czegoś, jeśli będzie na nim narysowanych więcej niż 3 warstwy. Za jego pomocą możesz sprawdzić swoje ulubione aplikacje. Zobaczysz, że nawet aplikacje z ponad miliardem pobrań mają czerwone obszary, więc po prostu uspokój się, gdy próbujesz zoptymalizować.
3. Renderowanie GPU
Jest to kolejne narzędzie z opcji programisty, zwane renderowaniem Profile GPU. Po wybraniu wybierz „Na ekranie jako paski”. Zauważysz kilka kolorowych pasków pojawiających się na ekranie. Ponieważ każda aplikacja ma osobne paski, co dziwne, pasek stanu ma swoje własne, a jeśli masz przyciski nawigacyjne oprogramowania, mają one również własne paski. W każdym razie paski są aktualizowane podczas interakcji z ekranem.
Paski składają się z 3-4 kolorów, a zgodnie z dokumentacją Androida ich rozmiar ma znaczenie. Im mniejszy, tym lepiej. Na dole masz kolor niebieski, który reprezentuje czas używany do tworzenia i aktualizowania list wyświetlania widoku. Jeśli ta część jest zbyt wysoka, oznacza to, że jest dużo niestandardowego rysunku widoku lub dużo pracy wykonanej w onDraw()
. Jeśli masz Androida 4.0+, zobaczysz fioletowy pasek nad niebieskim. Reprezentuje czas poświęcony na przesyłanie zasobów do wątku renderowania. Następnie pojawia się czerwona część, która reprezentuje czas spędzony przez renderer 2D Androida na wysyłanie poleceń do OpenGL w celu rysowania i przerysowywania list wyświetlania. Na górze znajduje się pomarańczowy pasek, który reprezentuje czas oczekiwania procesora na zakończenie pracy przez GPU. Jeśli jest za wysoki, aplikacja wykonuje zbyt dużo pracy na GPU.
Jeśli jesteś wystarczająco dobry, nad pomarańczą jest jeszcze jeden kolor. Jest to zielona linia reprezentująca próg 16 ms. Ponieważ Twoim celem powinno być uruchamianie aplikacji w 60 fps, masz 16 ms na narysowanie każdej klatki. Jeśli tego nie zrobisz, niektóre klatki mogą zostać pominięte, aplikacja może stać się niestabilna, a użytkownik na pewno to zauważy. Zwróć szczególną uwagę na animacje i przewijanie, tam płynność ma największe znaczenie. Mimo że za pomocą tego narzędzia możesz wykryć niektóre pominięte klatki, nie pomoże ci to w ustaleniu, gdzie dokładnie jest problem.
4. Przeglądarka hierarchii
To jedno z moich ulubionych narzędzi, ponieważ jest naprawdę potężne. Możesz go uruchomić z Android Studio poprzez Narzędzia -> Android -> Monitor urządzeń Android lub znajduje się również w folderze sdk/tools jako „monitor”. Można tam również znaleźć samodzielny plik wykonywalny hierarachyviewer, ale ponieważ jest przestarzały, należy otworzyć monitor. Jednak po otwarciu Monitora urządzeń z systemem Android przełącz się na perspektywę przeglądarki hierarchii. Jeśli nie widzisz żadnych uruchomionych aplikacji przypisanych do Twojego urządzenia, możesz zrobić kilka rzeczy, aby to naprawić. Spróbuj także sprawdzić ten wątek problemu, są ludzie z wszelkiego rodzaju problemami i wszelkiego rodzaju rozwiązaniami. Coś też powinno działać dla ciebie.
Dzięki Hierarchy Viewer możesz uzyskać naprawdę zgrabny przegląd hierarchii widoków (oczywiście). Jeśli widzisz każdy układ w osobnym pliku XML, możesz łatwo zauważyć bezużyteczne widoki. Jeśli jednak będziesz łączyć układy, łatwo może się to pomylić. Narzędzie takie jak to ułatwia wykrycie na przykład RelativeLayout, który ma tylko 1 dziecko, innego RelativeLayout. To sprawia, że jeden z nich można usunąć.
Unikaj wywoływania requestLayout()
, ponieważ powoduje to przechodzenie przez całą hierarchię widoków, aby dowiedzieć się, jak duży powinien być każdy widok. Jeśli jest jakiś konflikt z pomiarami, hierarchia może przechodzić wiele razy, co jeśli zdarzy się podczas jakiejś animacji, z pewnością spowoduje pominięcie niektórych klatek. Jeśli chcesz dowiedzieć się więcej o tym, jak Android rysuje swoje poglądy, możesz przeczytać ten. Przyjrzyjmy się jednemu widokowi w przeglądarce hierarchii.
W prawym górnym rogu znajduje się przycisk do maksymalizacji podglądu konkretnego widoku w samodzielnym oknie. Pod nim możesz również zobaczyć rzeczywisty podgląd widoku w aplikacji. Kolejną pozycją jest liczba, która reprezentuje ile dzieci ma dany widok, łącznie z samym widokiem. Jeśli wybierzesz węzeł (najlepiej główny) i naciśniesz „Uzyskaj czasy układu” (3 kolorowe kółka), będziesz miał wypełnione 3 kolejne wartości wraz z kolorowymi kółkami oznaczonymi jako miara, układ i rysowanie. Może nie być szokujące, że faza pomiaru reprezentuje czas potrzebny na zmierzenie danego widoku. Faza układu dotyczy czasu renderowania, podczas gdy rysowanie to rzeczywista operacja rysowania. Te wartości i kolory są względem siebie względne. Zielony oznacza, że widok jest renderowany w górnych 50% wszystkich widoków w drzewie. Żółty oznacza renderowanie w wolniejszych 50% wszystkich widoków w drzewie, czerwony oznacza, że dany widok jest jednym z najwolniejszych. Ponieważ wartości te są względne, zawsze będą czerwone. Po prostu nie możesz ich uniknąć.
Pod wartościami masz nazwę klasy, taką jak „TextView”, wewnętrzny identyfikator widoku obiektu oraz android:id widoku, który ustawisz w plikach XML. Zachęcam do wyrobienia nawyku dodawania identyfikatorów do wszystkich widoków, nawet jeśli nie odwołujesz się do nich w kodzie. Sprawi to, że identyfikacja widoków w Hierarchy Viewer będzie naprawdę prosta, a jeśli masz w swoim projekcie automatyczne testy, znacznie przyspieszy to targetowanie elementów. To zaoszczędzi trochę czasu Tobie i Twoim współpracownikom, którzy je napiszą. Dodawanie identyfikatorów do elementów dodawanych w plikach XML jest dość proste. Ale co z dynamicznie dodanymi elementami? Cóż, okazuje się, że to też jest naprawdę proste. Po prostu utwórz plik ids.xml w folderze wartości i wpisz wymagane pola. Może to wyglądać tak:
<resources> <item name="item_title" type="id"/> <item name="item_body" type="id"/> </resources>
Następnie w kodzie możesz użyć setId(R.id.item_title)
. Prościej się nie da.
Jest jeszcze kilka rzeczy, na które należy zwrócić uwagę podczas optymalizacji interfejsu użytkownika. Powinieneś generalnie unikać głębokich hierarchii, preferując płytkie, może szerokie. Nie używaj układów, których nie potrzebujesz. Na przykład prawdopodobnie można zastąpić grupę zagnieżdżonych LinearLayouts
RelativeLayout
lub TableLayout
. Możesz eksperymentować z różnymi układami, nie zawsze używaj LinearLayout
i RelativeLayout
. W razie potrzeby spróbuj również utworzyć niestandardowe widoki, co może znacznie poprawić wydajność, jeśli zostanie wykonane dobrze. Na przykład, czy wiesz, że Instagram nie używa TextViews do wyświetlania komentarzy?
Więcej informacji o Hierarchy Viewer można znaleźć na stronie Android Developers z opisami różnych okienek, za pomocą narzędzia Pixel Perfect itp. Jeszcze jedną rzeczą, na którą chciałbym zwrócić uwagę, jest przechwytywanie widoków w pliku .psd, co można zrobić za pomocą przycisk „Przechwyć warstwy okna”. Każdy widok będzie na osobnej warstwie, więc naprawdę łatwo jest go ukryć lub zmienić w Photoshopie lub GIMP-ie. Och, to kolejny powód, aby dodać identyfikator do każdego widoku, jaki możesz. Dzięki temu warstwy będą miały nazwy, które rzeczywiście mają sens.
W opcjach programisty znajdziesz dużo więcej narzędzi do debugowania, więc radzę je aktywować i zobaczyć, co robią. Co może pójść nie tak?
Witryna dla programistów Androida zawiera zestaw najlepszych praktyk dotyczących wydajności. Obejmują wiele różnych obszarów, w tym zarządzanie pamięcią, o czym tak naprawdę nie mówiłem. Po cichu to zignorowałem, ponieważ obsługa pamięci i śledzenie wycieków pamięci to zupełnie osobna historia. Korzystanie z biblioteki innej firmy do wydajnego wyświetlania obrazów bardzo pomoże, ale jeśli nadal masz problemy z pamięcią, sprawdź Leak canary stworzony przez Square lub przeczytaj to.
Zawijanie
To była dobra wiadomość. Złą nowością jest to, że optymalizacja aplikacji na Androida jest o wiele bardziej skomplikowana. Jest wiele sposobów na zrobienie wszystkiego, więc powinieneś znać ich wady i zalety. Zazwyczaj nie ma rozwiązania typu „srebrna kula”, które przyniesie same korzyści. Tylko dzięki zrozumieniu tego, co dzieje się za kulisami, będziesz w stanie wybrać najlepsze dla siebie rozwiązanie. Tylko dlatego, że Twój ulubiony programista mówi, że coś jest dobre, niekoniecznie oznacza to, że jest to dla Ciebie najlepsze rozwiązanie. Jest o wiele więcej obszarów do omówienia i więcej narzędzi do profilowania, które są bardziej zaawansowane, więc być może przejdziemy do nich następnym razem.
Upewnij się, że uczysz się od najlepszych programistów i najlepszych firm. Możesz znaleźć kilkaset blogów inżynierskich pod tym linkiem. To oczywiście nie tylko rzeczy związane z Androidem, więc jeśli interesuje Cię tylko Android, musisz filtrować konkretnego bloga. Gorąco polecam blogi Facebooka i Instagrama. Mimo że interfejs użytkownika Instagrama na Androidzie jest wątpliwy, ich blog inżynierski zawiera kilka naprawdę fajnych artykułów. Dla mnie to niesamowite, że tak łatwo jest zobaczyć, jak to się dzieje w firmach, które codziennie obsługują setki milionów użytkowników, więc nie czytanie ich blogów wydaje się szalone. Świat zmienia się bardzo szybko, więc jeśli nie próbujesz ciągle się doskonalić, uczyć się od innych i korzystać z nowych narzędzi, zostaniesz w tyle. Jak powiedział Mark Twain, osoba, która nie czyta, nie ma żadnej przewagi nad osobą, która nie potrafi czytać.