java.lang.OutOfMemoryError: Die Bitmap-Größe überschreitet das VM-Budget - Android


159

Ich habe eine Anwendung entwickelt, die viele Bilder auf Android verwendet.

Die App läuft einmal, füllt die Informationen auf dem Bildschirm ( Layouts, Listviews, Textviews, ImageViewsusw.) und Benutzer liest die Informationen.

Es gibt keine Animation, keine Spezialeffekte oder irgendetwas, das den Speicher füllen kann. Manchmal können sich die Zeichen ändern. Einige sind Android-Ressourcen und andere sind Dateien, die in einem Ordner auf der SDCARD gespeichert sind.

Dann beendet der Benutzer (die onDestroyMethode wird ausgeführt und die App bleibt von der VM im Speicher) und irgendwann tritt der Benutzer erneut ein.

Jedes Mal, wenn der Benutzer die App betritt, kann ich sehen, dass der Speicher immer größer wird, bis der Benutzer die App erhält java.lang.OutOfMemoryError.

Was ist der beste / richtige Weg, um mit vielen Bildern umzugehen?

Sollte ich sie in statische Methoden einfügen, damit sie nicht ständig geladen werden? Muss ich das Layout oder die im Layout verwendeten Bilder auf besondere Weise bereinigen?


4
Dies kann hilfreich sein, wenn sich viele Drawables ändern. Es funktioniert, seit ich das Programm selbst gemacht habe :) androidactivity.wordpress.com/2011/09/24/…

Antworten:


72

Es hört sich so an, als hätten Sie einen Speicherverlust. Das Problem besteht nicht darin, dass viele Bilder verarbeitet werden. Ihre Bilder werden nicht freigegeben, wenn Ihre Aktivität zerstört wird.

Es ist schwer zu sagen, warum dies so ist, ohne auf Ihren Code zu achten. Dieser Artikel enthält jedoch einige Tipps, die helfen könnten:

http://android-developers.blogspot.de/2009/01/avoiding-memory-leaks.html

Insbesondere die Verwendung statischer Variablen kann die Situation eher verschlechtern als verbessern. Möglicherweise müssen Sie Code hinzufügen, der Rückrufe entfernt, wenn Ihre Anwendung neu gezeichnet wird. Auch hier gibt es nicht genügend Informationen, um dies sicher zu sagen.


8
Dies ist wahr und auch viele Bilder (und kein Speicherverlust) können aus mehreren Gründen zu outOfMemory führen, z. B. weil viele andere Apps ausgeführt werden, Speicherfragmentierung usw. Ich habe gelernt, dass bei der Arbeit mit vielen Bildern outOfMemory angezeigt wird systematisch, wie immer das gleiche zu tun, dann ist es ein Leck. Wenn Sie es wie einmal am Tag oder so bekommen, liegt es daran, dass Sie zu nahe am Limit sind. Mein Vorschlag in diesem Fall ist, einige Dinge zu entfernen und es erneut zu versuchen. Auch der Bildspeicher befindet sich außerhalb des Heapspeichers. Achten Sie daher auf beide.
Daniel Benedykt

15
Wenn Sie einen Speicherverlust vermuten, können Sie die Heap-Nutzung schnell über den Befehl adb shell dumpsys meminfo überwachen. Wenn Sie feststellen, dass die Heap-Nutzung über einige GC-Zyklen zunimmt (GC_ * -Protokollzeile in Logcat), können Sie ziemlich sicher sein, dass Sie ein Leck haben. Erstellen Sie dann einen Heap-Dump (oder mehrere zu unterschiedlichen Zeiten) über adb oder DDMS und analysieren Sie ihn über das Dominator-Tree-Tool auf Eclipse MAT. Sie sollten ziemlich bald herausfinden, welches Objekt einen Speicherplatz hat, der mit der Zeit wächst. Das ist dein Speicherverlust. Ich habe hier einen Artikel mit einigen weiteren Details geschrieben: macgyverdev.blogspot.com/2011/11/…
Johan Norén

98

Einer der häufigsten Fehler bei der Entwicklung von Android-Apps ist der Fehler "java.lang.OutOfMemoryError: Bitmap-Größe überschreitet VM-Budget". Ich habe diesen Fehler häufig bei Aktivitäten mit vielen Bitmaps festgestellt, nachdem ich die Ausrichtung geändert habe: Die Aktivität wird zerstört, erneut erstellt und die Layouts werden aus dem XML-Code "aufgeblasen", wodurch der für Bitmaps verfügbare VM-Speicher belegt wird.

Bitmaps im vorherigen Aktivitätslayout werden vom Garbage Collector nicht ordnungsgemäß freigegeben, da sie Verweise auf ihre Aktivität gekreuzt haben. Nach vielen Experimenten habe ich eine recht gute Lösung für dieses Problem gefunden.

Legen Sie zunächst das Attribut "id" in der übergeordneten Ansicht Ihres XML-Layouts fest:

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

onDestroy() Rufen Sie dann in der Methode Ihrer Aktivität die unbindDrawables()Methode auf, die einen Verweis auf die übergeordnete Ansicht übergibt, und führen Sie dann a aus System.gc().

    @Override
    protected void onDestroy() {
    super.onDestroy();

    unbindDrawables(findViewById(R.id.RootView));
    System.gc();
    }

    private void unbindDrawables(View view) {
        if (view.getBackground() != null) {
        view.getBackground().setCallback(null);
        }
        if (view instanceof ViewGroup) {
            for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
            unbindDrawables(((ViewGroup) view).getChildAt(i));
            }
        ((ViewGroup) view).removeAllViews();
        }
    }

Diese unbindDrawables()Methode untersucht den Ansichtsbaum rekursiv und:

  1. Entfernt Rückrufe auf allen Hintergrundzeichnungen
  2. Entfernt Kinder in jeder Ansichtsgruppe

10
Außer ... java.lang.UnsupportedOperationException: removeAllViews () wird in AdapterView

Dank dieser Option kann ich meine Heap-Größe erheblich reduzieren, da ich viele Drawables habe ... @GJTorikian dafür versuchen Sie einfach, bei removeAllViews () zu fangen. Die meisten ViewGroup-Unterklassen funktionieren einwandfrei.
Eric Chen

5
Oder ändern Sie einfach die Bedingung: if (Instanz von ViewGroup anzeigen &&! (Instanz von AdapterView anzeigen))
Anke

2
@Adam Varhegyi, Sie können dieselbe Funktion für die Galerieansicht in onDestroy explizit aufrufen. Wie unbindDrawables (galleryView); hoffe, das funktioniert für Sie ...
hp.android


11

Um dieses Problem zu vermeiden, können Sie die native Methode verwenden, Bitmap.recycle()bevor Sie ein Objekt nullerstellen Bitmap(oder einen anderen Wert festlegen). Beispiel:

public final void setMyBitmap(Bitmap bitmap) {
  if (this.myBitmap != null) {
    this.myBitmap.recycle();
  }
  this.myBitmap = bitmap;
}

Und als nächstes können Sie ohne myBitmapAnruf ändern System.gc()wie:

setMyBitmap(null);    
setMyBitmap(anotherBitmap);

1
Dies funktioniert nicht, wenn Sie versuchen, diese Elemente einer Listenansicht hinzuzufügen. Hast du einen Vorschlag dazu?
Rafael Sanches

@ddmytrenko Sie recyceln das Bild, bevor Sie es zuweisen. Würde das nicht verhindern, dass seine Pixel gerendert werden?
IgorGanapolsky

8

Ich bin genau auf dieses Problem gestoßen. Der Haufen ist ziemlich klein, so dass diese Bilder in Bezug auf den Speicher ziemlich schnell außer Kontrolle geraten können. Eine Möglichkeit besteht darin, dem Garbage Collector einen Hinweis zum Sammeln von Speicher auf einer Bitmap zu geben, indem die Recyclingmethode aufgerufen wird .

Es wird auch nicht garantiert, dass die onDestroy-Methode aufgerufen wird. Möglicherweise möchten Sie diese Logik / Bereinigung in die Aktivität onPause verschieben. Weitere Informationen finden Sie im Diagramm / in der Tabelle zum Aktivitätslebenszyklus auf dieser Seite .


Danke für die Information. Ich verwalte alle Drawables und nicht Bitmaps, daher kann ich Recycle nicht aufrufen. Irgendwelche anderen Vorschläge für Drawables? Danke Daniel
Daniel Benedykt

Danke für den onPauseVorschlag. Ich verwende 4 Registerkarten mit BitmapBildern in allen, sodass die Zerstörung der Aktivität möglicherweise nicht zu früh erfolgt (wenn der Benutzer alle Registerkarten durchsucht).
Azurespot

7

Diese Erklärung könnte hilfreich sein: http://code.google.com/p/android/issues/detail?id=8488#c80

"Schnelle Tipps:

1) Rufen Sie NIEMALS System.gc () selbst auf. Dies wurde hier als Fix propagiert und funktioniert nicht. TU es nicht. Wenn Sie in meiner Erklärung bemerkt haben, dass die JVM vor dem Abrufen eines OutOfMemoryError bereits eine Garbage Collection ausführt, gibt es keinen Grund, eine weitere durchzuführen (dies verlangsamt Ihr Programm). Wenn Sie am Ende Ihrer Aktivität eine machen, wird das Problem nur vertuscht. Es kann dazu führen, dass die Bitmap schneller in die Finalizer-Warteschlange gestellt wird, aber es gibt keinen Grund, warum Sie nicht stattdessen einfach auf jeder Bitmap recyceln können.

2) Rufen Sie bei Bitmaps, die Sie nicht mehr benötigen, immer recycle () auf. Zumindest gehen Sie im onDestroy Ihrer Aktivität durch und recyceln Sie alle von Ihnen verwendeten Bitmaps. Wenn Sie möchten, dass die Bitmap-Instanzen schneller vom Dalvik-Heap erfasst werden, schadet es auch nicht, Verweise auf die Bitmap zu löschen.

3) Wenn Sie recycle () und dann System.gc () aufrufen, wird die Bitmap möglicherweise immer noch nicht vom Dalvik-Heap entfernt. Seien Sie nicht besorgt darüber. recycle () hat seine Arbeit erledigt und den nativen Speicher freigegeben. Es wird nur einige Zeit dauern, bis die zuvor beschriebenen Schritte ausgeführt sind, um die Bitmap tatsächlich vom Dalvik-Heap zu entfernen. Dies ist keine große Sache, da der große Teil des nativen Speichers bereits frei ist!

4) Nehmen Sie immer an, dass zuletzt ein Fehler im Framework vorliegt. Dalvik macht genau das, was er tun soll. Es ist vielleicht nicht das, was Sie erwarten oder was Sie wollen, aber es funktioniert so. ""


5

Ich hatte genau das gleiche Problem. Nach einigen Tests stellte ich fest, dass dieser Fehler bei der Skalierung großer Bilder auftritt. Ich habe die Bildskalierung reduziert und das Problem ist verschwunden.

PS Zuerst habe ich versucht, die Bildgröße zu reduzieren, ohne das Bild zu verkleinern. Das hat den Fehler nicht gestoppt.


4
Könnten Sie den Code darüber veröffentlichen, wie Sie die Bildskalierung durchgeführt haben? Ich stehe vor dem gleichen Problem und denke, dies könnte es lösen. Vielen Dank!
Gligor

@Gix - Ich glaube, er meint, dass er die Größe der Bilder reduzieren musste, bevor sie in die Projektressourcen aufgenommen wurden, wodurch der Speicherbedarf der Drawables verringert wurde. Verwenden Sie dazu den Fotoeditor Ihrer Wahl. Ich mag pixlr.com
Kyle Clegg

5

Die folgenden Punkte haben mir sehr geholfen. Es mag auch andere Punkte geben, aber diese sind sehr wichtig:

  1. Verwenden Sie nach Möglichkeit den Anwendungskontext (anstelle von activity.this).
  2. Stoppen Sie Ihre Threads und geben Sie sie in der Aktivitätsmethode onPause () frei
  3. Geben Sie Ihre Ansichten / Rückrufe in der Aktivitätsmethode onDestroy () frei

4

Ich schlage einen bequemen Weg vor, um dieses Problem zu lösen. Weisen Sie einfach das Attribut "android: configChanges" wie in der Mainfest.xml für Ihre fehlerhafte Aktivität angegeben zu. so was:

<activity android:name=".main.MainActivity"
              android:label="mainActivity"
              android:configChanges="orientation|keyboardHidden|navigation">
</activity>

Die erste Lösung, die ich herausgab, hatte die Häufigkeit von OOM-Fehlern wirklich auf ein niedriges Niveau reduziert. Das Problem wurde jedoch nicht vollständig gelöst. Und dann werde ich die 2. Lösung herausgeben:

Wie im OOM detailliert beschrieben, habe ich zu viel Laufzeitspeicher verwendet. Also reduziere ich die Bildgröße in ~ / res / drawable meines Projekts. Ein überqualifiziertes Bild mit einer Auflösung von 128 x 128 könnte beispielsweise auf 64 x 64 verkleinert werden, was auch für meine Anwendung geeignet wäre. Und nachdem ich dies mit einem Stapel Bilder getan habe, tritt der OOM-Fehler nicht mehr auf.


1
Dies funktioniert, weil Sie den Neustart Ihrer App vermeiden. Es ist also weniger eine Lösung als vielmehr eine Vermeidung. Aber manchmal ist das alles was Sie brauchen. Die Standardmethode onConfigurationChanged () in Activity dreht Ihre Ausrichtung für Sie um. Wenn Sie also keine Änderungen an der Benutzeroberfläche benötigen, um sich wirklich an die verschiedenen Ausrichtungen anzupassen, sind Sie möglicherweise vollkommen zufrieden mit dem Ergebnis. Achten Sie jedoch auf andere Dinge, die Sie neu starten können. Sie können dies verwenden: android: configChanges = "keyboardHidden | Ausrichtung | Tastatur | Gebietsschema | mcc | mnc | Touchscreen | screenLayout | fontScale"
Carl

3

Auch ich bin frustriert über den Outofmemory-Bug. Und ja, ich habe auch festgestellt, dass dieser Fehler beim Skalieren von Bildern häufig auftritt. Zuerst habe ich versucht, Bildgrößen für alle Dichten zu erstellen, aber ich habe festgestellt, dass dies die Größe meiner App erheblich vergrößert. Ich verwende jetzt nur ein Bild für alle Dichten und skaliere meine Bilder.

Meine Anwendung würde einen Outofmemory-Fehler auslösen, wenn der Benutzer von einer Aktivität zur nächsten wechselt. Das Setzen meiner Drawables auf null und das Aufrufen von System.gc () funktionierte nicht, ebenso wenig wie das Recycling meiner BitmapDrawables mit getBitMap (). Recycle (). Android würde beim ersten Ansatz weiterhin den Outofmemory-Fehler auslösen und bei jedem Versuch, eine recycelte Bitmap mit dem zweiten Ansatz zu verwenden, eine Canvas-Fehlermeldung auslösen.

Ich habe einen noch dritten Ansatz gewählt. Ich habe alle Ansichten auf null und den Hintergrund auf schwarz gesetzt. Ich mache diese Bereinigung in meiner onStop () -Methode. Dies ist die Methode, die aufgerufen wird, sobald die Aktivität nicht mehr sichtbar ist. Wenn Sie dies in der onPause () -Methode tun, sehen Benutzer einen schwarzen Hintergrund. Nicht ideal. Für die Methode onDestroy () gibt es keine Garantie, dass sie aufgerufen wird.

Um zu verhindern, dass ein schwarzer Bildschirm auftritt, wenn der Benutzer die Zurück-Taste auf dem Gerät drückt, lade ich die Aktivität in der onRestart () -Methode neu, indem ich die Methoden startActivity (getIntent ()) und finish () aufrufe.

Hinweis: Es ist nicht unbedingt erforderlich, den Hintergrund auf Schwarz zu ändern.


1

Die BitmapFactory.decode * -Methoden, die in der Lektion Effizientes Laden großer Bitmaps beschrieben werden , sollten nicht auf dem Haupt-UI-Thread ausgeführt werden, wenn die Quelldaten von der Festplatte oder einem Netzwerkspeicherort (oder einer anderen Quelle als dem Speicher) gelesen werden. Die Zeit, die diese Daten zum Laden benötigen, ist unvorhersehbar und hängt von einer Vielzahl von Faktoren ab (Lesegeschwindigkeit von der Festplatte oder vom Netzwerk, Größe des Images, Leistung der CPU usw.). Wenn eine dieser Aufgaben den UI-Thread blockiert, kennzeichnet das System Ihre Anwendung als nicht reagierend und der Benutzer hat die Möglichkeit, sie zu schließen (weitere Informationen finden Sie unter Entwerfen für Reaktionsfähigkeit).


0

Nun, ich habe alles versucht, was ich im Internet gefunden habe, und keiner von ihnen hat funktioniert. Das Aufrufen von System.gc () verringert nur die Geschwindigkeit der App. Das Recycling von Bitmaps in onDestroy hat auch bei mir nicht funktioniert.

Das einzige, was jetzt funktioniert, ist eine statische Liste aller Bitmaps, damit die Bitmaps nach einem Neustart überleben. Verwenden Sie einfach die gespeicherten Bitmaps, anstatt bei jedem Neustart der Aktivität neue zu erstellen.

In meinem Fall sieht der Code folgendermaßen aus:

private static BitmapDrawable currentBGDrawable;

if (new File(uriString).exists()) {
    if (!uriString.equals(currentBGUri)) {
        freeBackground();
        bg = BitmapFactory.decodeFile(uriString);

        currentBGUri = uriString;
        bgDrawable = new BitmapDrawable(bg);
        currentBGDrawable = bgDrawable;
    } else {
        bgDrawable = currentBGDrawable;
    }
}

Würde dies nicht Ihren Kontext verlieren? (Aktivitätsdaten)
Rafael Sanches

0

Ich hatte das gleiche Problem nur beim Wechseln der Hintergrundbilder mit angemessenen Größen. Ich habe bessere Ergebnisse erzielt, wenn ich ImageView auf null gesetzt habe, bevor ich ein neues Bild eingefügt habe.

ImageView ivBg = (ImageView) findViewById(R.id.main_backgroundImage);
ivBg.setImageDrawable(null);
ivBg.setImageDrawable(getResources().getDrawable(R.drawable.new_picture));

0

FWIW, hier ist ein leichter Bitmap-Cache, den ich codiert und seit einigen Monaten verwendet habe. Es ist nicht alles Schnickschnack, lesen Sie also den Code, bevor Sie ihn verwenden.

/**
 * Lightweight cache for Bitmap objects. 
 * 
 * There is no thread-safety built into this class. 
 * 
 * Note: you may wish to create bitmaps using the application-context, rather than the activity-context. 
 * I believe the activity-context has a reference to the Activity object. 
 * So for as long as the bitmap exists, it will have an indirect link to the activity, 
 * and prevent the garbaage collector from disposing the activity object, leading to memory leaks. 
 */
public class BitmapCache { 

    private Hashtable<String,ArrayList<Bitmap>> hashtable = new Hashtable<String, ArrayList<Bitmap>>();  

    private StringBuilder sb = new StringBuilder(); 

    public BitmapCache() { 
    } 

    /**
     * A Bitmap with the given width and height will be returned. 
     * It is removed from the cache. 
     * 
     * An attempt is made to return the correct config, but for unusual configs (as at 30may13) this might not happen.  
     * 
     * Note that thread-safety is the caller's responsibility. 
     */
    public Bitmap get(int width, int height, Bitmap.Config config) { 
        String key = getKey(width, height, config); 
        ArrayList<Bitmap> list = getList(key); 
        int listSize = list.size();
        if (listSize>0) { 
            return list.remove(listSize-1); 
        } else { 
            try { 
                return Bitmap.createBitmap(width, height, config);
            } catch (RuntimeException e) { 
                // TODO: Test appendHockeyApp() works. 
                App.appendHockeyApp("BitmapCache has "+hashtable.size()+":"+listSize+" request "+width+"x"+height); 
                throw e ; 
            }
        }
    }

    /**
     * Puts a Bitmap object into the cache. 
     * 
     * Note that thread-safety is the caller's responsibility. 
     */
    public void put(Bitmap bitmap) { 
        if (bitmap==null) return ; 
        String key = getKey(bitmap); 
        ArrayList<Bitmap> list = getList(key); 
        list.add(bitmap); 
    }

    private ArrayList<Bitmap> getList(String key) {
        ArrayList<Bitmap> list = hashtable.get(key);
        if (list==null) { 
            list = new ArrayList<Bitmap>(); 
            hashtable.put(key, list); 
        }
        return list;
    } 

    private String getKey(Bitmap bitmap) {
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        Config config = bitmap.getConfig();
        return getKey(width, height, config);
    }

    private String getKey(int width, int height, Config config) {
        sb.setLength(0); 
        sb.append(width); 
        sb.append("x"); 
        sb.append(height); 
        sb.append(" "); 
        switch (config) {
        case ALPHA_8:
            sb.append("ALPHA_8"); 
            break;
        case ARGB_4444:
            sb.append("ARGB_4444"); 
            break;
        case ARGB_8888:
            sb.append("ARGB_8888"); 
            break;
        case RGB_565:
            sb.append("RGB_565"); 
            break;
        default:
            sb.append("unknown"); 
            break; 
        }
        return sb.toString();
    }

}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.