Effizienter geht’s besser

Wie man MIDP-Anwendungen effizient programmiert

David Price und des Forum Nokia Team

Dieser Artikel befasst sich mit der effizienten Programmierung von MIDP-Anwendungen. Bei der Entwicklung von Anwendungen für Mobiltelefone müssen viele Aspekte berücksichtigt werden, die dem normalen Java-Programmierer auf den ersten Blick gar nicht auffallen oder ihm gar unwichtig erscheinen. Da wir es mit einer ganz anderen Plattform zu tun haben, muss man deren Eigenheiten kennen, um die Software speziell für diese Geräte optimieren und dem Anwender die bestmögliche Benutzerfreundlichkeit und Benutzererfahrung bieten zu können.

Dieser Artikel beschreibt, wie man ein MIDlet effizienter machen kann. Es sollen folgende Aspekte abgedeckt werden:

  • Ausführungsgeschwindigkeit
  • Größe der JAR-Datei (Download-Kosten usw.)
  • Verwendung von Ressourcen (Speicher, Networking)
  • wahrgenommene Performanz (Reaktionsfähigkeit des User Interface)

Der Artikel befasst sich speziell mit MIDP-Performanzfragen und geht nur sehr oberflächlich auf andere Java-Performanzprobleme ein, die nicht MIDP-spezifisch sind. Weitere Informationen zu diesen Themen findet man hier: Jack Shirazi: Java Performance Tuning. O’Reilly 2000 und Steve Wilson, Jeff Kesselman: Java Platform Performance: Strategies and Tactics. Addison-Wesley 2000.

Ausführungsgeschwindigkeit

Es gibt eine alte Programmierer-Faustregel, die besagt, dass Programme 90 Prozent ihrer Zeit mit der Ausführung von 10 Prozent ihres Codes verbringen. Aus diesem Grund hat man wesentlich mehr davon, wenn man, anstatt all seinen Code effizient zu machen, den Flaschenhals in seinem Code findet und diesen effizienter gestaltet.

Ein anderes weit verbreitetes Prinzip ist, dass ein bedachtes Design und die geschickte Auswahl von Algorithmen zu besseren Ergebnissen führt als eine Optimierung des Codes Zeile für Zeile. Dies gilt genauso für MIDlets wie für alle anderen Programme, und es ist sehr einfach, ein MIDlet durch unüberlegte Design-Entscheidungen auszubremsen.

Java-Programme verbringen gewöhnlich nur einen kurzen Teil ihrer Zeit mit dem Ausführen des eigenen Programmcodes und den größten Teil der Zeit mit der Ausführung des Library Code, der aufgerufen wird. Aus diesem Grund sollte man sich über die Performanz der Bibliotheken (vor allem der Grafikbibliotheken) informieren und sich gut überlegen, wie man diese aufruft.

Man sollte bedenken, dass sich die MIDP-Implementierungen unterschiedlicher Mobiltelefone signifikant in ihren Performanzeigenschaften voneinander unterscheiden, sodass der Ansatz, der auf dem einen Mobiltelefon die beste Performanz bringt, nicht zwangsläufig auch auf jedem anderen die optimale Lösung ist. Zum Beispiel kann eine bestimmte Library-Methode auf dem einen Mobiltelefon in Java implementiert sein, während sie auf einem anderen durch eine schnellere native Methode abgebildet wird.

Schließlich sollte man noch beachten, dass Performanzmessungen nicht nur zwischen verschiedenen Handymodellen variieren können, sondern auch zwischen verschiedenen Versionen des gleichen Modells. Hersteller führen üblicherweise Updates der Softwareversionen durch und manchmal werden sogar Hardwarekomponenten während des Produktlebenszyklus eines Gerätes ausgetauscht.

Messen der Ausführungsgeschwindigkeit

Das übliche Tool, um Performanz-Flaschenhälse in einem Programm aufzuspüren, ist ein Profiler. Normalerweise ist es jedoch nicht möglich, einen Profiler auf ein MIDlet anzusetzen, das auf einem Mobiletelefon ausgeführt wird, und das Profilen eines MIDlets in einem Emulator ist nicht sehr aussagekräftig, da die Flaschenhälse eines Emulators ganz andere sein können also die eines realen Endgerätes.

Der übliche Weg in einem MIDlet ist, die Messung selber durchzuführen, indem man ein paar Extrazeilen zu seinem Programmcode hinzufügt. Folgendermaßen lässt sich herausfinden, wie lange ein Methodenaufruf dauert:

long startTime = System.currentTimeMillis();
doSomething(); // der zu messende Aufruf
long timeTaken = System.currentTimeMillis() - startTime;

Dies liefert die Ausführungszeit des Methodenaufrufs in Millisekunden zurück. Um abweichende Ergebnisse aufgrund der Garbage Collection während des Tests zu vermeiden, kann man eventuell die Methode System.gc() aufrufen, bevor man den Test startet. Um die Messergebnisse des Tests auszugeben, kann man entweder einen speziellen MIDlet Screen verwenden oder die Ergebnisse durch Überschreiben des normalen Screens anzeigen.

Sie sollten sicher gehen, die Auflösung der Systemuhr des Handys zu überprüfen. Ihre Rückgabewerte können unter Umständen nicht Millisekundengenauigkeit haben, so dass man darauf achten sollte, ob der zurückgelieferte Wert zum Beispiel immer ein Vielfaches von zehn ist. In diesem Fall sollte man sicherstellen, dass der Test lang genug ist, sodass diese Ungenauigkeit kein Problem darstellt.

Grafikoperationen

Typischerweise spielt die Ausführungsgeschwindigkeit von Grafikoperationen bei MIDP-High-Level-Screen-Klassen wie Form und List keine Rolle, sondern lediglich bei der Low-Level-Klasse Canvas, die für Animationen und Spiele verwendet wird. Die Geschwindigkeit von Canvas-Grafikoperationen variiert erheblich zwischen verschiedenen Handys, da sie nicht allein von der darunter liegenden Hardware, sondern auch von der Effizienz der nativen Grafikbibliotheken des Gerätes abhängig ist.

Wenn lediglich ein kleiner Teil des Screens aktualisiert werden muss, sollte man einen Repaint mit folgender Methode veranlassen:

Canvas.repaint(int x, int y, int width, int height);

Dies ermöglicht es Ihnen anzugeben, dass nur der veränderte Bereich neu dargestellt wird. Die paint()-Methode sollte dann nur den Bereich neu darstellen, der durch das Clipping-Rechteck ihres Parameters Graphics angegeben wird, wodurch möglicherweise viele Berechnungen eingespart werden. Trotz allem sollte man sich darüber im Klaren sein, dass, wenn man Repaint-Anfragen schneller stellt, als das Gerät sie verarbeiten kann, es eventuell mehrere Anfragen zu einer zusammenfügt, indem es die paint()-Methode mit einem Clipping-Rechteck aufruft, dass die Flächen aller Aufrufe abdeckt. Wenn die Rechtecke viel Zwischenraum aufweisen, dann wird hierdurch auch viel Fläche abgedeckt, die kein Repaint benötigt.

Eine andere Art der Optimierung, die auf vielen Handys gut funktionieren sollte, ist das Verwenden eins Off-Screen Images, wenn der Screen sich zwischen den einzelnen Repaints nur leicht verändert. Man kann hierbei die Änderungen in dem Off-Screen Image ausgeben und sie dann beim Aufruf der paint()-Methode auf den Screen kopieren.

Garbage Collection

Man sollte stets vermeiden, unnötige Garbage-Objekte auf dem Heap zu erzeugen. Oft ist es einfacher, stattdessen existierende Objekte wieder zu verwenden. Ein spezielles Problem stellen so genannte Immutable-Objekte dar. Das sind Objekte, deren Zustand nach ihrer Erstellung nicht mehr verändert werden kann. Immutable-Objekte sind weit verbreitet, da sie große Vorteile haben, was die Zuverlässigkeit des Codes und die Thread Safety betrifft.

Trotz allem werden aber Immutable-Objekte schnell zu Garbage-Objekten, wenn ihr ursprünglicher Wert nicht mehr benötigt wird, zum Beispiel dann, wenn sich ihr ursprünglicher Wert verändert. Für jede Änderung muss ein neues Immutable-Objekt mit dem neuen Wert erzeugt werden und das alte verworfen werden, um dann durch den Garbage Collector entfernt zu werden. In MIDlets sind die Vorteile eines Immutable-Objektes oft nicht die Kosten wert und es ist besser, wieder verwendbare Objekte einzusetzen, denen neue Werte zugewiesen werden können.

Das bekannteste Beispiel hierfür ist die Klasse java.lang.String. Die meisten Programmierer vergessen einfach, wie viele normale Verwendungen von String das Entstehen von Garbage-Objekten zur Folge haben. Das klassische Beispiel hierfür ist die String-Konkatenation. Schauen Sie sich einmal die folgende Funktion zum Umkehren eines Strings an:

1: static String reverse(String s)
2: { 
3:    String t = "";
4:    for (int i = s.length()-1; i >= 0; --i)
5:       t += s.charAt(i);
6:    return t;
7: }

Die Zuweisung in Zeile 5 verändert nicht den String t, da t immutable ist, sondern sie erzeugt jedes mal einen neuen String, indem Sie den bestehenden Wert kopiert und ein neues Zeichen hinzufügt. Diese Methode erzeugt unnötigerweise s.lenght()-Garbage-Objekte. Dies ist ein klassisches Beispiel für das Problem mit Immutable-Objekten. Aber für Strings gibt es eine einfache Lösung: Die Klasse java.lang.StringBuffer ist ein „mutable“ Pendant zu String. Hiermit kann das vorhergehende Beispiel folgendermaßen wesentlich effizienter gestaltet werden:

1: static String reverse(String s)
2: { 
3:    StringBuffer t = new StringBuffer(s.length());
4:    for (int i = s.length()-1; i >= 0; --i)
5:    t.append(s.charAt(i));
6:    return t.toString();
7: }

Obwohl diese Probleme bestehen, sind die Vorteile von „immutable“ Objekten oft ihren Aufwand wert und das Erzeugen von einigen Garbage-Objekten ist keine große Sache, solange man nicht tausende davon pro Sekunde erzeugt. Selbst eine virtuelle MIDP-Maschine kann bequem tausende von Objekten pro Sekunde durch den Garbage Collector entfernen [Jack Shirazi: Java Performance Tuning. O’Reilly 2000].

Geschrieben von
David Price und des Forum Nokia Team
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.