Android – BitmapFactory und OutOfMemory-Exception

OutOfMemory-Exceptions sind immer eine unangenehme Sache. Trehten sie einmal auf, kann man nicht mehr reagieren. Das Programm wird einfach vom Stack geworfen und dem User eine unschöne Fehlermeldung vorgesetzt. Für den Entwickler ist die Fehlersuche langwierig, da das vermeintliche „Opfer“ selten der Verursacher ist. Der produzierte StackTrace ist schlicht unbrauchbar.

Die OOM-Exceptions trehten gehäuft im zusammenhang mit der BitmapFactory auf. Der Grund ist einfach, Bilder brauchen viel Platz…

Wenn man im Web nach Beispielen  für eine Bilder-Galerie sucht, bekommt man eigentlich immer das gleiche Beispiel. Ein paar kleine Bilder, lokale Resourcen, werden über eine Klasse die von BaseAdapter abgeleitet an ein über-gelagertes UI-Element gereicht. um dort zur Anzeige gebracht zu werden.

Bringt man jedoch Bilder auf einem Tablet zur Anzeige, werden schon die Bild-Dimensionen größer, nicht nur die Auflösungen. Auf einem 10 Zoll Tablet mit 1280×800 und mehr Bildpunkten braucht ein Bild in VollBild-Auflösung oder mehr schon mehrere MegaByte Ram, alleine zum Vorhalten der Bildinformationen. Dank der komprimierung brauchen Bilder als File wesentlich weniger Speicher, als die dann Tatsächlich im Ram belegen. Ein JPEG in 1920*1080 Aufgelöst und 500KB Dateigröße braucht mehr als 6MB Ram. Ein paar solcher Bilder im Ram abgelegt und die Dalvik-VM grüßt mit einer OutOfMemory-Exception.

Dazu kommt Fall einer Web Galerie die Bilder nicht lokal vorhanden sind, sondern bei Bedarf von einem WebServer geladen werden. Was einfach klingt, stellte sich schnell als vermeintliches Memory-Leak heraus. Im Grunde dreht sich alles um folgende Methode:

public View getView(int position, View cachedView, ViewGroup parent);

Mit dieser Methode werden die Views/darzustellenden Bilder zur Laufzeit abgefragt. Im einfachsten Fall sind das ImageViews die ein Bitmap auf den Bildschirm zeichnen.

Der augenscheinlichste Ansatzpunkt zum Speicher sparen ist, dass man eine vorher benutze View zur „Wiederverwendung“ übergeben bekommt (cachedView). Die CachedView kann man auch benutzen, der Resourcengewinn ist bei großen Bildern aber nicht „von Interesse“. In Anbetracht der xMB pro Bild, kommt es auf die paar Byte für die ImageView Struktur kaum an. Kommt jedoch ein AsyncTask zum ein Einsatz, um die Bilddaten „im Hintergrund“ herunterzuladen und in ein Bitmap zu decodieren, dann ist diese cachedView immens wichtig. Slided der User schneller durch die Galerie, als die Bilder aus dem Netz herunter geladen werden können, werden AsyncTask erzeugt, die „sinnlos“ Bilder im Hintergrund herunter laden und decodieren. Bei kleinen Bildern und einem Lokalen Cache, ist das nicht mal schlecht. Einmal gecached, werden die Bilder das nächste mal aus der lokalen Quelle geladen. Bei den großen Bildern frisst das parallele Decodieren schlicht zu viel Speicher.

Bei der GalDroid-Entwicklung hat es sich nicht als sinnvoll herausgestellt, die Anzahl der Threads zu kontrollieren. Der overhead, die Downloads zu queuen, stand in keinem Verhältnis zum Nutzen oder der Wartezeit des Users. Ich hab mir in der CachedView einfach den Task gemerkt, der das Bild herunterlädt und diesen bei Bedarf abgebrochen. So sind bei einer VollBild-Anzeigen selten mehr als vier Bilder parallel in Bearbeitung.

Eine Ausnahme gibt es noch: Ist das Layout des UI-Elements nicht statisch fixiert, werden die Dimension geändert, oder hängen die Dimensionen gar von den darzustellenden Inhalt ab, wird das 0-te Element mehrfach abgefragt um die benötigten Dimension zu bestimmen. Das ist in sofern übel, da es 5-7 mal hinter einander passierte, ohne das jeweils eine cachedView übergeben wird. Wieder: bei kleinen Bildern unschön, bei großen Bildern grüßt die OutOfMemory-Exception.

Das Problem kann man umgehen, in dem man mehr speichert. Bei der GalDroid wird einfach ein Feld von WeakReferences angelegt, in dem jedes je angefragte ImageView gespeichert wird. Die Logik ist simpel – wird ein Element mehrfach kurz hinter einander abgefragt, sollte es in diesem Speicher zu finden sein. Wird es länger Zeit nicht genutzt, räumt der GC auf und die WeakReference zeigt auf NULL.

Sollte nach diesen Maßnahmen immer noch OOM-Exceptions auftreten, kann man noch Bitmap.recycle aufrufen, sobald eine ImageView aus dem Sichtbereich des Users verschwindet. Das ist aber „frickelig“, da nicht immer klar ist, wann etwas „nicht mehr gesehen werden kann“. Das Recycle gibt sofort den Speicher des Bitmaps wieder frei, ohne den GC aufzurufen. Allerdings muss man so jedes mal das Bitmap neu laden, wenn es wieder in den Sichtbereich kommt.

Sollte das alles nicht helfen, kann man auf den Allocation Tracker zurückgreifen. Dieses Tool ist in dem ADT für Eclipse enthalten und listet jedes erstellte Objekt zur Laufzeit auf.