Android: Einfache Perfomance-Optimierungen

Artikeldaten
Erste Version 0.1 (22.01.2012)
Aktuelle Version 0.32(02.08.2012)

1. Kurzbeschreibung

Auf den mobilen Geräten sind die Rechenleistung und der Akku die teuersten Einheiten, da beide die Benutzdauer des Gerätes ohne Steckdose begrenzen. Aus diesem Grund sollte jeder Entwickler seine App so weit optimieren, dass diese den Akku und andere mobile Ressourcen schont. Dieser Beitrag zeigt einige der Möglichkeiten dazu.

2. Einfache Optimierungen

  1. Zugriff auf Klassenvariablen (Eigenschaften / Felder) sollte, wenn möglich (z.B. innerhalb der Klasse oder des Packages), direkt über die Klassenvariable statt über die Getter und Setter-Methoden erfolgen. Bei externen Schnittstellen und Zugriffen ist dieses Vorgehen natürlich nicht anwendbar.
    Die Performance-Steigerung liegt ca. beim Faktor 2-3.
  2. Bei den Operationen mit Strings (Konkatenation) sollte man den StringBuilder einsetzen, statt des „+“ Operators. Der Performance-Verlust beim „+„-Operator steigt exponentiell zur Anzahl der Konkatenationen, da bei jeder Operation ein neues temporäres String-Objekt erzeugt werden muss. Beim StringBuilder steigt der Rechenaufwand dagegen nur linear an.
  3. Unnötige Objekterzeugungen sollen vermieden werden. So z.B.:
    1. Objekterzeugung innerhalb einer Schleife
    2. Mehrfache Objekterzeugung / Referenzierung auf oft verwendete Objekte (findViewById(), DBHelper() usw.)
  4. Schleifen sollen optimiert werden. Statt:
    for (int i = 0; i < array.length; i++) { … }

    sollte

    int arrayLenght = array.length;
    for (int i = 0; i < arrayLength; i++) { … }

    verwendet werden.

  5. Bei der Erstellung des Layouts können viele Fehler und unnötige Verschachtelungen auftreten. Diese können mit dem mitgelieferten Tool layoutopt analysiert und optimiert werden.
  6. Größere Arrays (mit mehr als 100 Elementen) sind deutlich performanter als ArrayList-Objekte.
  7. Sehr einfache Klassen (die nur wenige Eigenschaften und sonst nichts beinhalten), sollten, soweit es möglich ist, durch Arrays ersetzt werden, da dadurch die Objekterzeugung entfällt.
  8. An den Stellen, wo es möglich und sinnvoll ist, sollen Variablen als final oder eventuell sogar als final static definiert werden. Solche Variablen können bereits von Compiler optimiert und / oder initialisiert werden, sodass zur Laufzeit weniger Ressourcen und Rechenleistung für diese Objekte notwendig sind.
  9. Die Suchschleifen, nach dem das gewünschte Ergebnis ermittelt wurde, sollten sofort über break oder continue verlassen werden, um das unnötige Durchlaufen der restlichen Elemente zu vermeiden.
  10. Rechen- oder zeitintensive Aufgaben (Datei- oder Netzwerkzugriffe, Berechnungen) sollen immer in einen eigenen Thread ausgelagert werden (mit Services, AsyncTasks, Loadern oder eigenen Thread-Implementierungen). Dadurch bleibt die App-Oberfläche weiterhin für den Benutzer bedienbar.
  11. Bei WebServices und anderen Internet-Schnittstellen soll dem JSON-Format immer der Vorzug vor dem XML-Format gegeben werden. JSON-Format hat weniger Overhead und spart dem Benutzer nicht nur die Downloadzeit, sondern auch das Datenvolumen.
  12. Um die Datei-Größe der fertigen APK-Datei klein zu halten (und somit auch der installierten App), empfiehlt es sich den mitgelieferten Obfuscator ProGuardzu nutzen. Dieser ersetzt die langen Variablen und Methodennamen durch kürzere (z.B.:
    public String getMyPerfectValue(String oldValue)

    wird zu

    public String a(String b)

    ) und führt damit zu kompakteren Dateien.

  13. Wenn Sie mehrere Algorithmen zur Auswahl haben, um Ihre Aufgabe zu lösen, testen Sie diese auf dem Gerät und wählen Sie eine, die auf diesem am performantesten läuft (Benchmarks).
  14. Zuletzt können Flaschenhälse in der App-Performance mithilfe des TraceView (DDMS) lokalisiert werden.
  15. Bei SQL-Abfragen sollen nur die wirklich notwendigen Daten (Spalten und Datenmenge) abgefragt werden.
  16. Set ADT Version 16 kommt noch ein weiteres Tool hinzu. Lint hilf unbenutzte Ressourcen und Problemzonen beim Zugriff auf APIs (unterschiedliche Versionen) zu finden.

3. Weiterführende Ressourcen

3.1. Interne Ressourcen

3.2. Bücher

Android: Erste App (Währungsrechner) – Teil 2

Den ersten Teil des Workshops finden Sie unter „Android: Erste App (Währungsrechner) – Teil 1„.

Unsere App ist bereits lauffähig. Aber es fehlt noch ein entscheidender Punkt, bevor wir uns der Programmlogik annehmen. Die beiden Auswahlboxen sind momentan ohne Inhalt und somit zwecklos. Das wollen wir natürlich ändern.

Die Auswahlboxen können als Inhalt Daten aus einer Datenbank oder aus einem Array (Auflistung) entgegennehmen. Da wir später noch unsere App mehrsprachig machen wollen, werden wir die Währungen als Ressourcen ablegen.

Dafür befolgen Sie die folgenden Schritte:

  1. Klicken Sie mit der rechten Maustaste auf das Projekt in der Package-Explorer Ansicht.
  2. Wählen Sie den Punkt „Android Tools“ (ganz unten im Kontextmenü).
  3. Wählen Sie im Untermenü „New Resource File ...„.
  4. Geben Sie als File „arrays“ ein.
  5. Wählen Sie als Typ „Values“ aus.
  6. Klicken Sie auf „Finish„.

Damit legen wir eine neue Ressource-Datei, in der wir Auflistungen speichern werden. Wir benötigen insgesamt 2 Auflistungen. In der neuen Datei legen Sie entweder über die graphische Oberfläche über den „Add“ Knopf ein neues StringArray (siehe Bilder) mit dem Namen „waehrung„, oder Sie geben die Werte direkt als XML-Code ein. Die XML-Darstellungen sehen Sie weiter unten.

0.1. array.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="waehrung">
        <item>EUR - Euro</item>
        <item>USD - Amerikanische Dollar</item>
        <item>AUD - Australische Dollar</item>
        <item>RUB - Russische Rubel</item>
        <item>CUD - Kanadische Dollar</item>
        <item>GBP - Englische Pfund</item>
    </string-array>
    <string-array name="waehrung_kurs">
        <item>1</item>
        <item>1.3795</item>
        <item>1.3314</item>
        <item>42.1941</item>
        <item>1.3495</item>
        <item>0.8738</item>
    </string-array>
</resources>

Nun können wir die beiden Auswahllisten mit dem Inhalt der ersten Auslistung füllen. Die zweite Liste benötigen wir später im Code. Es sind die Währungskurse in Bezug auf Euro, die wir für die Umrechnung benötigen.

Um die Auswahlliste an eine Auflistung zu binden, müssen wir nur in unseren Layout-Datei „main.xml“ bei den beiden Auswahllisten (Spinner) folgende Zeile (Zeile Nr. 6) hinzufügen.

<Spinner
	android:layout_height="wrap_content"
	android:layout_width="0dp"
	android:layout_weight="1"
	android:id="@+id/selWaehrungStart"
	android:entries="@array/waehrung">
</Spinner>

Wenn Sie jetzt die App starten, können Sie bereits die Ausgangs- und Zielwährung wählen (siehe Bild).

Nun können wir uns den Java-Quellcode widmen. Als Erstes sollte unser „Berechnen“ Knopf auf das Klicken reagieren. Dazu registrieren wir einen Listener (Zuhörer), der praktisch darauf wartet, bis der Benutzer eine Aktion ausführt. Dazu müssen wir als Erstes die Instanz des Knopfes ermitteln. Nun kommt uns zugute, dass wir unsere Views mit einer eindeutigen ID versehen haben.

Aus dem Java Code zu einer Activity könne wir jederzeit eine Referenz auf ein View mit einer ID ermitteln. Dafür stellt die Activity die Methode findViewById(ID). Da die Methode die Basisklasse „View“ zurückliefert, müssen wir den Rückgabewert noch auf das zu erwartende View (in unserem Fall Button) kasten (Button cmdBerechnen = (Button)findViewById(R.id.cmdBerechnen);). Wie an diesem Beispiel zu sehen ist, greifen wir auf die automatisch generierte „R„-Klasse, die uns die Referenzen auf unsere Ressourcen zur Verfügung stellt. Dabei sind folgende oft verwendete Referenzen wichtig:

  • R.id : Referenzen auf die Views der Layouts. Im Layout sind das die android:id="" Attribute
  • R.layout : Referenzen auf die Layout-Dateien
  • R.string : Referenzen auf unsere definierten Texte
  • R.array : Referenzen auf unsere definierte Auflistungen
  • R.color : Referenzen auf die Farben
  • R.drawable : Referenzen auf die Bilder
  • und einige weitere. Weitere Infos dazu finden Sie unter: developer.android.com

Den Listener fügen Sie mit der folgen Zeile hinzu:

cmdBerechnen.setOnClickListener(new OnClickListener() {
	public void onClick(View v) {
		kursBerechnen();
	}
});

Wir rufen in diesem unsere eigene Methode auf, die die Berechnung und die Ausgabe übernimmt. Das Ganze passiert in der onCreate Methode, die beim Erstellen einer Activity als Erstes aufgerufen wird (Lebenszyklen einer Activity wird in einem anderen Workshop erklärt). Schauen wir uns diese genauer an.

0.2. HauptActivity.java

package de.webducer.android.wrechner;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;

public class HauptActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        Init();
    }

    /**
     * Initialisierung des Menüs (wird nur ein mal aufgerufen)
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
    	MenuInflater inflater = new MenuInflater(this);
    	inflater.inflate(R.menu.menu, menu);
    	return super.onCreateOptionsMenu(menu);
    }

    /**
     * Reagieren auf ein Klick auf ein Menüeintrag
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
    	switch (item.getItemId()) {
    	// Ergebnisliste löschen
		case R.id.optLoeschen:
			TextView ergebnisse = (TextView)findViewById(R.id.txtErgebnisse);
			ergebnisse.setText("");

			break;

		// Programm schließen
		case R.id.optSchliessen:
			this.finish();

			break;

		default:
			break;
		}
    	return true;
    }

    /**
     * Initialisierung der Oberfläche
     */
    private void Init(){
    	// Button mit einem Listener verbinden
    	Button cmdBerechnen = (Button)findViewById(R.id.cmdBerechnen);
    	cmdBerechnen.setOnClickListener(new OnClickListener() {
			public void onClick(View v) {
				kursBerechnen();
			}
		});
    }

    /**
     * Berechnung und Ausgabe des Ergebnisses
     */
    private void kursBerechnen(){
    	// Elemente auslesen
    	EditText txtBetrag = (EditText)findViewById(R.id.txtBetrag);
    	TextView txtErgebnis = (TextView)findViewById(R.id.txtErgebnisse);
    	Spinner selWaehrungStart = (Spinner)findViewById(R.id.selWaehrungStart);
    	Spinner selWaehrungEnde = (Spinner)findViewById(R.id.selWaehrungEnde);

    	Boolean failure = false;

    	// Uwandlung des Betrages in eine Zahl
    	String betragText = txtBetrag.getText().toString();
    	Double betrag = 0d;
    	if (betragText != null && betragText != "") {
    		try {
    			betrag = Double.parseDouble(betragText);
			} catch (NumberFormatException e) {
				Toast.makeText(this, R.string.err_betarg_format, Toast.LENGTH_LONG).show();
				failure = true;
			}
		}

    	// Bestimmen der Ausgangs- und Zielwährung
    	int wahrungStart = selWaehrungStart.getSelectedItemPosition();
    	int waehrungEnde = selWaehrungEnde.getSelectedItemPosition();

    	// Bestimmen des jeweiligen Wechselkurses
    	String kursStartText = getResources().getStringArray(R.array.waehrung_kurs)[wahrungStart];
    	String kursEndeText = getResources().getStringArray(R.array.waehrung_kurs)[waehrungEnde];

    	// Umwandlung des Kurses in eine Zahl
    	Double kursStart = 0d;
    	Double kursEnde = 0d;
    	if (kursStartText != null && kursStartText != "" && !failure) {
			try {
				kursStart = Double.parseDouble(kursStartText);
			} catch (NumberFormatException e) {
				Toast.makeText(this, R.string.err_kurs_format, Toast.LENGTH_LONG).show();
				failure = true;
			}
		}
    	if (kursEndeText != null && kursEndeText != "" && !failure) {
			try {
				kursEnde = Double.parseDouble(kursEndeText);
			} catch (NumberFormatException e) {
				Toast.makeText(this, R.string.err_kurs_format, Toast.LENGTH_LONG).show();
				failure = true;
			}
		}

    	if(!failure){
	    	// Umrechnung
	    	Double ergebnis = betrag / kursStart * kursEnde;

	    	// Ergebnis ausgeben
	    	StringBuilder ergebnisText = new StringBuilder();
	    	ergebnisText.append(getResources().getStringArray(R.array.waehrung)[wahrungStart].subSequence(0, 3))
	    		.append(" ")
	    		.append(String.format("%.2f", betrag))
	    		.append(" = ")
	    		.append(getResources().getStringArray(R.array.waehrung)[waehrungEnde].substring(0, 3))
	    		.append(" ")
	    		.append(String.format("%.2f", ergebnis))
	    		.append("\n");
	    	txtErgebnis.setText(txtErgebnis.getText() + ergebnisText.toString());
    	}
    }
}

In der Zeile 21 wird durch den Aufruf der Methode setContentView(R.layout.main); wiesen wir unseren Activity HauptActivity.java ein Layout zu, den wir wieder über die generierte Klasse „R“ ansprechen.

Der Aufruf unserer eigenen Methode Init() in der Zeile 23 setzt den Listener für unseren Berechnungs-Knopf. Nun wenden wir uns der eigentlichen Berechnung zu (Zeilen 75 bis 142).

Als Erstes holen wir uns die Referenzen zu allen Views, die wir für unsere Berechnung benötigen (Betrag, Ausgangswährung, Endwährung und Ergebnisfeld).

In der Zeile 87 erhalten wir durch den Aufruf der Methode getText() den Betrag für die Umrechnung. Das ist allerdings ein Textfeld und wir benötigen eine Zahl. Aus diesem Grund konvertieren wir den erhaltenen Text in ein Double-Format (mit der vorherigen Prüfung, ob der Text überhaupt eingegeben wurde, Zeile 89).

In den Zeilen 99 und 100 erhalten wir den Index des ausgewählten Eintrages aus den Auswahlboxen der Währung. Jetzt kommt uns der zweite Array, in dem wir die Kurse definiert haben, zugute. Dieser ist genauso von der Reihenfolge her aufgebaut, wie das Währungs-Array. Nun können wir durch den Index aus den Zeilen 99 und 100 die Kurse ermitteln (Zeilen 103 und 104). Diese sind wiederum Texte und müssen zuerst in Double konvertiert werden, wie bereit beim Betrag.

Wenn alle Konvertierungen ohne Fehler durchgelaufen sind, berechnen wir nun in der Zeile 128 den umgerechneten Betrag. Um eine schöne Anzeige zu erhalten, formatieren wir nur ein wenig unsere Ausgabe. Die Anzeige soll dann in etwa folgendermassen aussehen:

EUR 125,00 = USD 172,44

Damit wir auch eine Geschichte unserer Berechnungen erhalten, hängen wir in der Zeile 140 unser Ergebnis an den vorhandenen Text an.

Um dem Benutzer zu signalisieren, dass mit der Berechnung etwas schief gelaufen ist, erzeugen wir bei einem Konvertierungsfehler einen Toast (kleine Benachrichtigung, die kurz auf dem Bildschirm erscheint). Das erfolgt in den Zeilen 93, 113 und 121. Wenn Sie einen solchen Fehler provozieren wollen, tauschen Sie einfach in der array.xml-Datei in dem Werte-Array die Punkte durch die Kommas aus (erzeugt Fehler beim Konvertieren).

1. Lokalisierung

Die Lokalisierung ist unter Android relativ einfach gelöst. Die Ressourcen, die keinen Länder-Postfox haben, gehören zu der Standardsprache (in unseren Fall Deutsch). Die Standardsprache muss immer vollständig sein. Weitere Sprachen dagegen nicht. Wird eine spezielle Übersetzung nicht gefunden, wird die der Standard-Übersetzung genommen.

In unserer App müssen die Daten aus der strings.xml und arrays.xml Dateien übersetzt werden. Dafür klicken Sie mit der rechten Maustaste auf das Projekt und wählen Sie Android Tools -> New Ressource File... . Wie im Bild zu sehen ist, muss für neue Sprache nun unter „Available Qualifiers“ „Language“ auswählen und auf der rechten Seite den zweistelligen Ländercode eingeben („en“ für Englisch). Nun kann der Inhalt der beiden Dateien in die neuen kopiert werden und die Werte übersetzt werden. Die neuen Dateien landen im Ordner „res/values-en„.

1.1. strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
	<!-- Globale Strings -->
	<string name="app_name">Currency Calculator</string>

	<!-- View Texte -->
    <string name="betrag_eingeben">Please type the value to calculate!</string>
    <string name="cmd_berechnen">Calculate</string>

    <!-- Menü -->
    <string name="opt_loeschen">Clear Result List</string>
    <string name="opt_loeschen_kurz">Clear</string>
    <string name="opt_schliessen">Close App</string>
    <string name="opt_schliessen_kurz">Close</string>

    <!-- Fehlertexte -->
    <string name="err_betarg_format">The value has wrong format!</string>
    <string name="err_kurs_format">The currency value has wrong format!</string>
</resources>

1.2. arrays.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="waehrung">
        <item>EUR - Euro</item>
        <item>USD - American Dollar</item>
        <item>AUD - Australian Dollar</item>
        <item>RUB - Russian Rubel</item>
        <item>CUD - Canadian Dollar</item>
        <item>GBP - Great Britain Pound</item>
    </string-array>
</resources>

2. Menü

Nun gehen wir an das Menü. Dafür erstellen wir wieder eine neue Android Ressource-Datei von Typ Menü. Diese XML-Datei landet im Ordner „res/menu„.

<?xml version="1.0" encoding="utf-8"?>
<menu
  xmlns:android="http://schemas.android.com/apk/res/android">
    <item
    	android:title="@string/opt_loeschen"
    	android:titleCondensed="@string/opt_loeschen_kurz"
    	android:id="@+id/optLoeschen"
    	android:icon="@drawable/opt_clear" />
    <item
    	android:title="@string/opt_schliessen"
    	android:titleCondensed="@string/opt_schliessen_kurz"
    	android:id="@+id/optSchliessen"
    	android:icon="@drawable/opt_close" />
</menu>

In unseren Activity-Java-Datei müssen nur noch wenige Zeilen hinzugefügt werden, um dem Menü ein Leben einzuhauchen. In den Zeilen 30 bis 34 wir das Menü initialisiert (ähnlich, wie das Layout in der onCreate-Methode). Dafür dient die überschriebene Methode onCreateOptionsMenu(). Unter Optionsmenü versteht man unter Android das Menü, das durch das Betätigen der Menütaste erscheint.

Die Auswertung, welches der Menüpunkte nun ausgewählt wurde, erfolgt in der überschriebenen Methode onOptionsItemSelected(). Über die Item-ID können wir bestimmen, was nun ausgewählt wurde und abhängig davon unseren Code ausführen (hier ist ein CASE-Konstrukt sehr gut geeignet, um die Menüpunkte zu unterscheiden und den Code übersichtlich zu halten). Für das Schließen unseren App benötigen wir nur eine Zeile mit this.finish() (Zeile 51). Um die Ergebnisse zurückzusetzen, müssen wir wieder die Referenz auf das View holen und deren Text auf einen leeren Text setzen (Zeilen 44 und 45).

Damit ist unsere App so weit funktionsfähig und einsatzbereit. Die Kurse können sehr einfach in der XML-Datei angepasst und erweitert werden.

Mehrere gleichnamige Zertifikate unter Thunderbird nutzen

Zertifikatübersicht
Zertifikatübersicht

Heute bin ich mit einem Problem gestoßen. Ich habe mir für meine zwei Haupt-Email-Adressen Zertifikate bei trustcenter.de bestellt.  Naturgemäß lauten beide auf meinen Namen. Theoretisch sollte es damit auch keine Probleme geben, da beide ja auf unterschiedliche Email-Adressen ausgestellt sind.

Auf dem Mac unter Mail gab es auch keine Probleme. Beide Zertifikate wurden problemlos importiert und konnten zum Signieren und Verschlüsseln benutzt werden.

Problem

Zertifikatzuordnung
Zertifikatzuordnung

Auf dem EeePC bin ich vor Kurzem von Windows Live-Mail auf Thunderbird 3.0 umgestiegen. Der Import verlief auch ohne Probleme und es wurden beide Zertifikate in der Übersicht angezeigt. Das Problem taucht auf, wenn man versucht die Zertifikate den einzelnen Email-Adressen zuzuweisen. In der Drop-Down-Liste erscheint nur ein einziges Zertifikat statt der beiden importierten.

Einzeln importiert funktionieren diese problemlos. Zusammen importiert, wird nur ein Zertifikat angezeigt.

Nach langer Recherche konnte die Ursache in der Bildung der Drop-Down-Liste von Thunderbird festgestellt werden. Zur Bildung der Liste werden bei Thunderbird nur Vor- und Nachnahme benutzt (CN), aber nicht die erweiterten Felder des Zertifikates. Viele andere Zertifikathersteller schreiben in die CN noch weitere Daten, sodass das beschriebene Problem meines Wissens nur bei trustcenter.de auftritt. Die Auszeichnung, wie trustcenter.de dies tut, entsprich der Empfehlung des Zertifikatstandards.

Lösung

Allgeminer Name
Allgeminer Name

Um das Problem bei Thunderbird und trustcenter.de zu umschiffen, habe ich eins von meinen Zertifikaten gesperrt und ein neues beantragt. Dieses Mal aber in dem Feld Vorname nicht nur meinen Vornamen eingegeben, sondern „Herr …“.

Nach dem neuen Import hat Thunderbird nun beide Zertifikate richtig aufgelistet und ich konnte diese den jeweiligen Email-Adressen zuordnen.

Alternativer Name
Alternativer Name