Kurs Android (10)

Autor: Damian Chodorek • Opublikowany: 12 czerwca 2015 • Ostatnia aktualizacja: 12 sierpnia 2015 • Kategoria: android, kursy

Jak zoptymalizować aplikację na Androida?
Artykuł znajdziesz również w wydaniu Software Developer’s Journal.

Po raz kolejny mój artykuł pojawił się na łamach magazynu Software Developer’s Journal. Gorąco zachęcam więc do odwiedzenia strony sdjournal.pl oraz do zapoznania się z treścią magazynu.

Mój artykuł znajduje się na stronie 14, ale warto przeczytać (a przynajmniej przejrzeć) całe wydanie.

Poniżej zamieściłem prawie ten sam artykuł.

Wstęp

Na temat optymalizacji aplikacji tworzonych na Androida napisano grube książki. W tym krótkim artykule postaram się przedstawić najpopularniejsze sposoby oraz narzędzia, które służą do poprawiania wydajności. Po przeczytaniu tego tekstu będziesz w stanie podjąć konkretne kroki, aby zwiększyć wydajność swojej aplikacji. Zdobędziesz także ogólne pojęcie o narzędziach, które pomogą Ci zidentyfikować wszelkie problemy, których być może nie jesteś nawet świadom.

Istnieje ogromna różnica pomiędzy aplikacją, która działa nieźle, a taką, która działa doskonale. Tworząc program na system iOS spodziewamy się, że będzie on uruchamiany na urządzeniach, które są do siebie podobne. Mam na myśli rozmiar ekranu, sprzęt, system operacyjny.

Z systemem Android sprawa wygląda całkiem inaczej. Aplikacja musi radzić sobie z dużymi rozbieżnościami związanymi z wielkością ekranu, procesorem, wersją API i wszystkimi innymi smaczkami dotyczącymi konkretnych urządzeń.

Wydajność aplikacji jest szczególnie ważna, ponieważ urządzenia z systemem Android charakteryzują się mniejszą mocą obliczeniową niż komputery stacjonarne czy laptopy. Co więcej, poważnym aspektem jest zużycie energii. Niestety, jeżeli chodzi o pojemność baterii w naszych smartfonach, to prawo Moore’a tutaj nie obowiązuje.

Oficjalne porady i sztuczki

Na oficjalnej stronie poświęconej tworzeniu aplikacji na system Android zaproponowano developerom szereg wartościowych mikrooptymalizacji, do których powinni się stosować. Artykuł znajduje się pod adresem http://developer.android.com/training/articles/perf-tips.html i myślę, że stanowi świetne uzupełnienie tego poradnika. Poniżej zamieszczam krótkie streszczenie zaprezentowanych tam zasad.

  • Unikaj tworzenia zbędnych obiektów, zwłaszcza jeśli chodzi o typ String warto rozważyć korzystanie z klasy StringBuilder.
  • Tam gdzie to możliwe, korzystaj z metod statycznych. Czas ich wywołania jest około 15-20% krótszy.
  • Twórz stałe przy użyciu modyfikatorów static oraz final.
  • Unikaj korzystania z Getterów i Setterów wewnątrz metod klas. Używaj bezpośrednich odwołań do pól.
  • Standardowo korzystaj z pętli for-each.
  • Zamiast tworzenia prywatnej klasy wewnętrznej, rozważ umieszczenie jej w pakiecie.
  • Staraj się używać liczb całkowitych zamiast zmiennoprzecinkowych.
  • Korzystaj z bibliotek zamiast pisać własny kod.
  • Kod stworzony przy pomocy Android NDK niekoniecznie wykona się szybciej niż ten napisany w Javie.
  • Mierz osiągi aplikacji podczas dokonywania optymalizacji.

Bardziej szczegółowe informacje na temat powyższych porad znajdziesz w artykule, którego adres podałem wcześniej.

Usprawnij UI

Z jednej strony aplikacja powinna charakteryzować się pięknym i zachęcającym wyglądem. Z drugiej strony użytkownicy oczekują, że UI będzie w pełni responsywny. Dlatego tak ważne jest odpowiednie zaprojektowanie layoutów. Hierarchia widoków powinna być maksymalnie płaska. To znaczy, programiści powinni unikać zbytniego zagnieżdżania elementów.

Zwłaszcza jeżeli chodzi o LinearLayout, przy pomocy którego łatwo projektować UI, ale niewiele trzeba, aby wpaść w pułapkę stworzenia złożonej hierarchii. To z kolei ma fatalny wpływ na wydajność aplikacji. Dużo lepszym wyjściem jest wykorzystanie elementu RelativeLayout. Dzięki niemu często można stworzyć dokładnie tak samo wyglądający, ale bardziej wydajny layout (Listing 1).

Listing 1 (XML). Zastąpienie LinearLayout przy pomocy RelativeLayout.

<!-- zagnieżdżony LinearLayout -->

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:orientation="horizontal" >

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="@string/hello_world" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="end"
            android:text="@string/hello_world" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:gravity="bottom"
        android:orientation="horizontal" >

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="@string/hello_world" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="end"
            android:text="@string/hello_world" />
    </LinearLayout>

</LinearLayout>

<!-- wersja bardziej wydajna – spłaszczona hierarcha -->

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true"
        android:text="@string/hello_world" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignParentTop="true"
        android:text="@string/hello_world" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentStart="true"
        android:text="@string/hello_world" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentEnd="true"
        android:text="@string/hello_world" />

</RelativeLayout>

Zmniejsz rozmiar pliku APK

Aby zaciemnić i zoptymalizować kod możemy użyć takich narzędzi jak Proguard, DexGuard oraz DashO. Pliki wynikowe zawierają dużo niepotrzebnego kodu, zwłaszcza jeżeli dołączone są biblioteki zewnętrzne. Zadaniem wspomnianych programów jest usunięcie zbędnych klas, pól i metod. Ponadto usuwają kod potrzebny przy debugowaniu oraz zastępują nazwy krótkimi sekwencjami znaków.

Wykorzystanie tego typu programów powoduje także przyspieszenie wykonania kodu. Domyślnie Proguard uruchamiany jest w Eclipse podczas tworzenia wersji release. Na listingu 2 pokazano jak skonfigurować program, aby pozbył się kodu odpowiedzialnego za komunikaty loggera.

Listing 2. Przykładowa konfiguracja programu Proguard.

-assumenosideeffects class android.util.Log {
	public static *** d(...);
}

Jeżeli aplikacja korzysta z grafik w formacie PNG, można zoptymalizować ich rozmiar przy użyciu programów OptiPNG lub PNGCrush. Wszelkie zasady związane z optymalizacją grafiki dla stron internetowych są jak najbardziej pożądane jeżeli chodzi o aplikacje na Androida. Przykładowo, można spróbować wykorzystać format JPG lub zastosować binarną transparencję zamiast 8-bitowej.

Optymalizacja odśmiecania

Rozumienie tego jak działa Garbage Collector jest kluczową kwestią jeżeli chodzi o wydajność. Kod aplikacji uruchamiany jest wewnątrz maszyny wirtualnej Dalvik. Kiedy obiekty nie są już dłużej potrzebne, pamięć jest zwalniana przez Garbage Collector. Ta operacja powoduje zatrzymanie działania aplikacji na czas nawet do 200 ms.

Aby Garbage Collector działał rzadko i krótko, należy unikać zbędnej alokacji obiektów i korzystać z tych, które już istnieją. Dobrym przykładem jest własna implementacja adaptera listy. Jedną z metod, które powinniśmy przesłonić jest getView(). Jej zadaniem jest tworzenie i zwracanie obiektów widoku elementów listy.

Należy stworzyć tylko tyle obiektów, ile elementów listy jest widocznych na ekranie. Następnie przy pomocy parametru convertView, należy ponownie wykorzystać utworzone obiekty zmieniając jedynie treść, którą prezentują (Listing 3).

Listing 3 (Java). Wykorzystanie parametru convertView w metodzie getView().

public class MyAdapter extends BaseAdapter {
    
    /* ... */
    
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        // sprawdzamy, czy referencja jest pusta
        // jeżeli tak, należy przypisać do niej nowy obiekt
        if (convertView == null) {
            LayoutInflater mInflater = (LayoutInflater) context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
            LinearLayout item = (LinearLayout) mInflater.inflate(R.layout.inner_grid_item, parent, false);
            convertView = item;
        }
        
        // jeżeli convertView został zaalokowany,
        // należy zmienić tekst, który wyświetla
        TextView textView=(TextView)convertView.findViewById(R.id.inner_grid_text);
        textView.setText("nowy tekst");
        
        return convertView;
    }
}

W Androidzie 5.0 wprowadzono środowisko uruchomieniowe ART oraz nowy, zoptymalizowany Garbage Collector. Dzięki temu proces odśmiecania pamięci przestał powodować przestoje w działaniu aplikacji.

Korzystaj z wydajnych struktur danych

Android dostarcza klasy Sparse*Array. Na listingu 4 przedstawiono w jaki sposób można uniknąć tworzenia zbędnego obiektu Integer przy pomocy klasy SparseArray.

Listing 4 (Java). Użycie klasy SparseArray.

// mniej wydajna struktura
Map map = new HashMap();

// wykorzystanie wydajnej struktury danych
SparseArray map = new SparseArray();
map.put(1, "data"); 

Wydajny format jest szczególnie ważny podczas pobierania danych z serwera. Warto rozważyć zastosowanie formatów binarnych, w których można jednocześnie umieścić dane binarne oraz tekstowe, aby ograniczyć ilość zapytań do serwera.

Pamiętaj o indeksowaniu

Podczas korzystania z bazy danych SQLite należy rozważyć utworzenie indeksów dla kolumn i tabel, które są często przeszukiwane. Indeksy przyspieszają wykonywanie zapytań SELECT. Z drugiej strony, spowalniają wykonywanie operacji modyfikujących tabele (Listing 5).

Listing 5 (SQL). Przykładowe utworzenie indeksów.

CREATE INDEX idx ON  tbl;

/* tylko dla jednej kolumny */
CREATE INDEX idx ON tbl (clmn);

/* unikalne indeksy dbają o integralność danych */
CREATE UNIQUE INDEX idx on tbl (clmn);

/* indeks kompozytowy */
CREATE INDEX idx on tbl (clmn1, clmn2);

Nie należy stosować indeksów dla:

  • niewielkich tabel,
  • tabel często aktualizowanych lub uzupełnianych,
  • kolumn, które zawierają dużą liczbę wartości NULL,
  • kolumn często modyfikowanych.

Monitoruj stan połączenia internetowego

Serwisy działające w tle oraz alarmy są bardzo często używane w celu okresowego pobierania danych z internetu. Jeżeli jednak telefon nie jest połączony z siecią lub połączenie jest zbyt wolne, po co w ogóle zlecać pobieranie?

Przy pomocy klasy ConnectivityManager możemy sprawdzić czy połączenie z internetem jest dostępne, a jeśli tak, to jakiego jest rodzaju (Listing 6).

ConnectivityManager transmituje sygnał CONNECTIVITY_ACTION ("android.net.conn.CONNECTIVITY_CHANGE") jeżeli nastąpiła zmiana stanu połączenia. Można więc zarejestrować broadcast receiver <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>, którego zadaniem będzie nasłuchiwanie wspomnianych zmian. Przy jego pomocy możemy zatrzymać lub zlecić dokonanie aktualizacji.

Listing 6 (Java). Przykład sprawdzenia połączenia internetowego.

// sprawdzenie stanu połączenia
ConnectivityManager cm = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
 
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
boolean isConnected = activeNetwork != null && activeNetwork.isConnectedOrConnecting();
					  
// sprawdzenie rodzaju połączenia
boolean isWiFi = activeNetwork.getType() == ConnectivityManager.TYPE_WIFI;

Lint

To narzędzie wprowadzone w ADT 16. Jego zadaniem jest przeskanowanie plików źródłowych projektu pod kątem potencjalnych problemów i możliwości optymalizacji. Można z niego korzystać zarówno w wierszu poleceń, jak i w środowisku Eclipse czy IntelliJ.

Poniżej lista przykładowych błędów, które można wykryć:

  • brakujące i niewykorzystane tłumaczenia,
  • problemy z optymalizacją layoutów,
  • niewykorzystane zasoby,
  • problemy z grafiką (brakujące rozmiary, duplikaty, złe wymiary, itp.),
  • błędy w pliku AndroidManifest.

Aby przeskanować projekt w Eclipse, wystarczy wybrać Android Tools, a następnie Run Lint: Check for Common Errors. Jeżeli chodzi o wiersz poleceń, należy wpisać nazwę programu oraz podać katalog z projektem jako parametr: lint myproject.

Traceview

To narzędzie przeznaczone do graficznej reprezentacji logów wykonania aplikacji. Dzięki niemu możemy zmierzyć czas działania poszczególnych metod oraz zidentyfikować problemy z wydajnością. Z programu Traceview można korzystać zarówno w wierszu poleceń, jak i w Eclipse.

Aby wykorzystać narzędzie, należy najpierw wygenerować pliki logów. Można to zrobić na dwa sposoby.

  • Wykorzystując metody startMethodTracing() oraz stopMethodTracing(). Jest to dokładniejszy sposób (Listing 7).
  • Wykorzystując DDMS (Dalvik Debug Monitor Server). Ten sposób jest mniej dokładny.

Aby włączyć narzędzie w Eclipse, należy wybrać Window, Open Perspective, DDMS. Następnie należy wybrać proces, który nas interesuje i kliknąć ikonę Start Method Profiling. Należy kliknąć w tę samą ikonę, aby zatrzymać profilowanie i otrzymać wyniki.

Na listingu 7 parametr mystring oznacza, że dane będą zapisane w katalogu sdcard/mystring.trace. Ponieważ wykorzystana jest karta SD, należy do pliku AndroidManifest dodać pozwolenie WRITE_EXTERNAL_STORAGE. Po uruchomieniu aplikacji i wygenerowaniu logów, należy je skopiować przy pomocy polecenia adb pull /sdcard/mystring.trace, a następnie przeanalizować wykorzystując polecenie traceview mystring.

Listing 7 (Java). Profilowanie przy użyciu metod startMethodTracing() oraz stopMethodTracing().

android.os.Debug.startMethodTracing("mystring");

// kod...

android.os.Debug.stopMethodTracing(); 

Hierarchy Viewer

To bardzo przydatne narzędzie, którego zadaniem jest graficzne przedstawienie hierarchii widoków, z których zbudowany jest UI oraz analiza ich wydajności. Przy jego pomocy możemy znaleźć niepotrzebne warstwy hierarchii, co znacznie ułatwia proces optymalizacji.

Aby skorzystać z programu, wystarczy wpisać w wierszu poleceń hierarchyviewer, a następnie wybrać emulator oraz aktywność, którą chcemy zbadać. Jeżeli chodzi o Eclipse, to należy wybrać Window, Open Perspective, Other, a następnie wybrać z listy Hierarchy View.

Dla każdego węzła pojawią się trzy kropki. Pierwsza reprezentuje czas potrzebny na obliczenie rozmiaru widoku. Druga reprezentuje czas potrzebny na stworzenie widoku, a trzecia czas potrzebny na jego narysowanie (Rysunek 1).

Rysunek 1. Przykładowa hierarchia widoków w programie Hierarchy Viewer

  • Kolor zielony oznacza, że widok jest szybszy niż 50% wszystkich widoków w hierarchii.
  • Żółty oznacza, że widok jest wolniejszy niż 50% widoków w hierarchii.
  • Czerwony oznacza, że widok jest najwolniejszy, czyli potrzebuje najwięcej czasu ze wszystkich.

Podsumowanie

W artykule dokonałem podsumowania najważniejszych narzędzi, które developerzy wykorzystują do optymalizacji aplikacji tworzonych na system Android. Z pewnością istnieje wiele innych programów, jednak znajomość tych podstawowych jest niezbędna, a opisanie wszystkich wykracza poza tematykę tego krótkiego artykułu.

Zaproponowałem także różnego rodzaju mikrooptymalizacje oraz dobre praktyki programowania, dzięki którym można znacząco przyspieszyć działanie aplikacji. Należy pamiętać, że optymalizowanie jest procesem czasochłonnym, który może mieć negatywny wpływ na czytelność kodu. Na każdym etapie należy dokonywać profilowania różnych aspektów aplikacji i sprawdzać jak alternatywne rozwiązania wpływają na wydajność.

Źródła

część 11

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.