Apps für Android programmieren leicht gemacht!
Performance verbessern - Android App

Performance verbessern

Performance Optimierungen sind neben den Fehlerbehebungen eines der großen A & O’s, nach der Fertigstellung einer App.

Nachdem man seine App nun endlich fertig gestellt hat, muss man manchmal beim Test auf schwächeren Geräten feststellen, dass die App langsam läuft. Die einzige Chance diesem Problem auf den Grund zu gehen ist eine Performance Verbesserung. Doch wie setzt man diese um und wo fängt man damit an?

In diesem Artikel möchte ich euch gerne beibringen wie ihr eure Apps unter Java optimiert, Fehler korrigiert und zukünftig besser mit Resourcen umgehen könnt.

Performance verbessern heißt nicht Funktionen zu entfernen oder eine einfache App zu erstellen, sondern die bestehenden Algorithmen zu optimieren und gegebenenfalls zu ersetzen.

 

Background Thread:

Background Threads eigenen sich perfekt für alle Programmieranweisungen, die nicht direkt mit der GUI der App zusammenhängen.

Warum?
Ganz einfach, Background Threads oder auch Hintergrundprozesse verlaufen Asynchron.
Normalerweise wird in einem Programmcode eine Zeile ausgeführt und anschließend dann die nächste Zeile. In einem Background Thread wird einfach ein neues Unterprogramm geöffnet, es ist unabhängig von der Zeilenposition des Ursprungsprogramms. Die App und der Background Thread laufen nebeneinander. Sie sind also von einander unabhängig.

Wir sollten also Dinge, die keinen direkten Einfluss auf die Anzeige oder das Aussehen der App haben und Rechenaufwändig oder Umweltbedingungen ausgesetzt sind, in einen Background Thread auslagern. Da wir sozusagen ein Unterprogramm unserer App öffnen kann es passieren, dass unsere App bereits an einer anderen Stelle ist, wie unser Background Thread. Aus eben diesem Grund ist es nicht direkt möglich das Aussehen der App aus diesem heraus zu ändern.

Ein Beispiel:
Wir nehmen nun an, dass wir zu Beginn unserer App einen Background Thread starten, um Asynchron ein Bild aus dem Internet zu laden. Je nach Internetverbindung (Umwelteinfluss) kann dies kürzer oder länger dauern. Also können wir nicht genau bestimmen an welchem Punkt im Programmablauf sich nun die App befindet. Es kann also passieren, dass der Background Thread in dem Moment fertig wird das Bild zu laden, an dem der Benutzer schon längst die Seite gewechselt hat. Ergo können/dürfen wir an diesem Punkt nicht versuchen das Bild in der App anzuzeigen.

 

Unnötige Variablen entfernen:

Variablen zu erstellen und zu nutzen kostet Android Zeit. Zeit ist dabei Arbeit und Arbeit ist Akkuverbrauch.
Eine einzelne Variable zu erstellen mag nicht viel erscheinen, aber falsch und an ungünstigen Stellen eingesetzt bedeutet dies Performaceverluste. Nicht nur, dass die Variablen erstellt werden müssen, es muss der Speicherplatz reserviert werden und am Ende muss dann noch der Garbage Collector die alten Variablen wieder aus dem Arbeitsspeicher löschen.
Man sollte somit in der Regel bestrebt sein die Anzahl der Rechenoperationen und damit verbundenen Variablendefinierungen zu minimieren.

Ein Beispiel:

// Eine ganz schlechte Programmierung, mit viel zu vielen Variablen und Schritten.
public int dauer(int stunden){
   int dauerSekunden = 0;
   int sekunde = 1;
   int minuteSekunden = sekunde * 60;
   int stundeSekunden = minuteSekunden * 60;

   dauerSekunden = stundeSekunden * stunden;

   return dauerSekunden;
}

// Besser
public int dauer(int stunden){
   int dauerSekunden = 0;

   dauerSekunden = 3600 * stunden;

   return dauerSekunden;
}


// Noch besser
public int dauer(int stunden){
   return 3600 * stunden;
}

Diese paar Variablen mehr oder weniger scheinen nicht schlimm, aber stellt euch mal vor, wir hätten einen Datensatz von 2.000 Daten und durchlaufen diese mit einer Schleife. Für jeden Schleifendurchlauf müssen wir die Methode „dauer“, aus unserem Beispiel, einmal aufrufen. Das macht im schlimmsten Fall schon 10.000 Variablen und 6.000 Operationen, im Gegensatz zu 2.000 Variablen und 2.000 Operationen im besten Fall.

2.000 Datensätze sind in der Realität übrigens keine Seltenheit. Eine solche Optimierung kann die App unter Umständen vor einem Absturz bewahren und mehrere Millisekunden einsparen.

 

Listener statt Schleifen:

Schleifen hin oder her, aber Alles kann man mit diesen Helferlein nicht lösen.
Nun gut, ich muss zugeben, wenn wir versuchen wollen würden auf die Ausführung eines Events zu warten, dann könnte man sogar versuchen dies mit einer Schleife zu realisieren. Auf gut Deutsch: Wir laufen so lange durch die Schleife, bis etwas passiert oder passiert ist.

Das Problem an der Geschichte ist, dass diese Schleifen unheimlich schnell und damit oft durchlaufen.
Das zieht unheimlich viel Rechenleistung und damit Akku. Leider ist dies ein häufiger Anfängerfehler.

Die bessere Idee wäre es Listener zu nutzen.

Ein Beispiel:

// Ganz miserable Programmierung
Button btn = (Button) findViewById(R.id.button);
while(btn.isPressed() == false){
    // Java bleibt so lange in dieser Schleife, bis der Knopf gedrückt wird.
}
Log.d("Button", "Wurde geklickt.");

// Vieeeel besser
Button btn = (Button) findViewById(R.id.button);
btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.d("Button", "Wurde geklickt.");
    }
});

 

Timer statt Schleifen:

Die Idee ist ähnlich anzusiedeln, wie die Nutzung einer Schleife für ein Event.
Wir gehen davon aus, dass wir vor der Schleife eine bestimmte Zeit festlegen, nach der etwas geschehen soll.
Ist diese Zeit noch nicht erreicht bleiben wir innerhalb der Schleife und prüfen erneut.

Auch dieser Weg ist nicht wirklich zu empfehlen. Android durchläuft diese Schleife schneller als wir uns vorstellen können, verbraucht dabei Unmengen an Ressourcen und hängt sich im schlimmsten Fall sogar auf.
Wir sollten an dieser Stelle einen Timer nutzen.

timer = new Timer();    
refresher = new TimerTask() {
   public void run() {
         //Hier wird etwas aufgeführt (in 10 Sekunden)
   };
};
//Timer, Funktion startet in 10 Sekunden (Angabe in Millisekunden)
timer.schedulee(refresher, 10000);

 

Niemals schlafen:

Wir könnten versuchen die App für wenige Sekunden schlafen zu legen, um eine Funktion in wenigen Sekunden auszuführen.
Ich empfehle diese Vorgehensweise niemals zu nutzen. Es könnte passieren, dass eure App für die angegebene Zeit einfriert.
Benutzt stattdessen den oben angesprochenen Timer.

Ein Beispiel:

try {
    Thread.sleep(10000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

 

Berechnungen kürzen – Komplexität senken:

 

Die Laufzeit einer Funktion wird in O(n) angegeben (Ausgesprochen: O von n).
Andere Laufzeiten wären Beispielsweise O(n^2), O(log n), O(2n), … .
N beschreibt hier die Anzahl der zu durchlaufenden Daten.
Diese Laufzeit ist immer approximiert, also geschätzt oder näherungsweise beschrieben.
Bestimmt wird sie durch die Anzahl der rechenintensiven Berechnungen innerhalb dieser Funktion.

Wenn es also n zu durchlaufende Daten gibt und wir für jede Date eine Berechnung durchführen, dann ergibt sich eine Komplexität/approximierte Laufzeit von O(n).
Die beste zu erreichende Lauzeit ist O(n).
Versucht nach Möglichkeit die Anzahl der Berechnungen so gering wie möglich zu halten.

Wenn wir nun für jede Zahl in einem Array die Anzahl in Sekunden angeben wollen (alle Zahlen sind Tage), dann können wir entweder rechnen:
„Tage * 24 * 60 * 60“ oder „Stunden * 86400“.
Ich lasse euch selber entscheiden, welche Berechnung schneller geht.

 

Kleinere Ressourcen nutzen:

Was ich genau mit kleinen Ressourcen meine ist, dass man versuchen sollte seine verwendeten Bilder genau auf die Displaygröße anzupassen.
Ein Display der nur eine Auflösung von 640×480 Pixeln hat, der kann kein 1920×1080 Pixel großes Bild anzeigen, ohne es vorher kleiner zu skalieren. Zumal bei größeren Bildern auch mehr an Datenmenge im RAM anfällt. Dies zieht nur unnötig Leistung.

Um unser Ziel zu erreichen, erstellen wir einfach mehrere Ordner und verschieben die Bilder in unterschiedlicher Größe in diese. Android sucht sich dann automatisch die Bilder aus dem zur Displaygröße passenden Ordner aus.  Die Ordner müssen heißen: drawable-mdpi, drawable-hdpi, drawable-xhdpi, drawable-xxhdpi, drawable-xxxhdpi. Aufsteigend nach der Größe sortiert.

Ein Beispiel:
Wir haben ein Handy dessen Größe der Konvention „hdpi“ entspricht.
Wir haben außerdem folgende Ordnerstruktur:

  • drawable-mhdpi/bild.jpg (640×480)
  • drawable-hdpi/bild.jpg (820×640)
  • drawable-xhdpi/bild.jpg (1024×768)
  • drawbale-xxhdpi/bild.jpg (1240×1060)
  • drawble-xxxhdpi/bild.jpg (1920×1680)

Es sind nun also die Datei bild.jpg aus dem Ordner „drawble-hdpi“ benutzt. Dieses Bild hat eine Auflösung von 820×640 Pixeln. Wir sparen im Gegensatz zu dem Originalbild von 1920×1680 also Ressourcen, obwohl das Bild weiterhin auf dem Display groß genug ist, um nicht zu verpixeln.

 

Fazit:

Mit ein paar kleinen Handgriffen und dem nötigen Wissen, was man vermeiden sollte, kann man viele Performanceprobleme lösen.
Mir ist wohl bewusst, dass dieser Artikel nur einen Bruchteil aller möglichen Performanceverbesserungen anschneidet, aber für den Anfang sollten diese reichen.

Ich werde mich in nächster Zeit noch einmal daran setzten weitere Vorschläge und Möglichkeiten aufzuschreiben und einen weiteren Artikel daraus machen.

Marvin

Ich bin 23 Jahre jung und studiere zurzeit Wirtschaftsinformatik an der Georg-August-Universität in Göttingen. Ich bin ein Mensch, der sich neben der Programmierung noch für tausend andere Dinge interessiert, die mal mehr und mal weniger verrückt sind. Vor allem aber bin ich Feuer und Flamme mit der Programmierung von eigenen kleinen Apps und Programmen, die mein Leben bereichern.

3 Kommentare

*Pflichtfeld

  • und was ist mit Methoden, die Methoden aufrufen, welche Methoden aufrufen. (Ihr wisst was ich meine :)) Das kann man doch auch in eine schreiben, WENN man es nur einmal braucht, und nicht überall seperat abrufbar sein muss. 🙂

    Daniel

  • Hallo Daniel,
    Was du dort beschreibst schimpft sich Rekursion.
    Rekursion habe ich absichtlich nicht mit einbezogen, da Rekusrion bis zu einem Gewissen Punkt der Iteration überlegen ist. Außerdem ist die Rekursion meistens besser zu verstehen und einfacher zu programmieren.

    Die Alternative wäre die Nutzung von Schleifen in Schleifen (Iteration), was auf Dauer aber ebenfalls sehr langsam ist.

    Gruß,
    Marvin

  • Hallo,
    du hast die Background Threads hier erwähnt, kannst du evtl einen Artikel darüber machen, wie das zu programmieren ist?

    Gruß
    Daniel