wake-up-neo.com

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

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

Die App wird einmal ausgeführt, füllt die Informationen auf dem Bildschirm (Layouts, Listviews, Textviews, ImageViews usw.) und der Benutzer liest die Informationen.

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

Dann wird der Benutzer beendet (die onDestroy -Methode wird ausgeführt und die App bleibt im Speicher von VM) und dann gibt der Benutzer irgendwann wieder ein.

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

Was ist also die beste/richtige Art, mit vielen Bildern umzugehen?

Soll 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?

157
Daniel Benedykt

Es klingt wie Sie ein Speicherleck haben. Das Problem besteht darin, dass nicht viele Bilder verarbeitet werden. Ihre Bilder werden nicht freigegeben, wenn Ihre Aktivität zerstört wird.

Es ist schwer zu sagen, warum das so ist, ohne auf Ihren Code zu achten. Dieser Artikel enthält jedoch einige Tipps, die möglicherweise hilfreich sind:

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, mit dem Rückrufe entfernt werden, wenn Ihre Anwendung neu gezeichnet wird. Auch hier sind jedoch nicht genügend Informationen vorhanden, um dies mit Sicherheit zu sagen.

70
Trevor Johns

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 gefunden, nachdem ich die Ausrichtung geändert habe: Die Aktivität wird zerstört, neu erstellt und die Layouts werden aus dem XML-Code "aufgeblasen", wodurch der für Bitmaps verfügbare Speicherplatz "VM" belegt wird.

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

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"
     >
     ...

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

    @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 durchsucht den Ansichtsbaum rekursiv und:

  1. Entfernt Rückrufe bei allen Hintergrund-Drawables
  2. Entfernt Kinder in jeder Ansichtsgruppe
96
hp.android

Um dieses Problem zu vermeiden, können Sie die systemeigene Methode Bitmap.recycle() vor null- Bitmap verwenden (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 myBitmap ändern, ohne System.gc() aufzurufen:

setMyBitmap(null);    
setMyBitmap(anotherBitmap);
10
ddmytrenko

Diese Erklärung könnte helfen: http://code.google.com/p/Android/issues/detail?id=8488#c8

"Schnelle Tipps:

1) NIEMALS selbst System.gc () aufrufen. Dies wurde hier als Fehlerbehebung verbreitet 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, dies erneut zu tun (dies verlangsamt Ihr Programm). Wenn Sie am Ende Ihrer Aktivität einen machen, wird das Problem nur vertuscht. Dies kann dazu führen, dass die Bitmap schneller in die Finalizer-Warteschlange gestellt wird. Es gibt jedoch keinen Grund, warum Sie nicht stattdessen einfach für jede Bitmap die Option recycle aufgerufen haben könnten.

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

3) Wenn Sie recycle () und dann System.gc () aufrufen, wird die Bitmap möglicherweise immer noch nicht aus dem Dalvik-Heap entfernt. Seien Sie nicht besorgt darüber. recycle () hat seinen Job gemacht und den nativen Speicher freigegeben. Es wird nur einige Zeit dauern, bis ich die zuvor beschriebenen Schritte ausgeführt habe, um die Bitmap tatsächlich vom Dalvik-Haufen 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 es zuletzt einen Fehler im Framework gibt. Dalvik tut genau das, was es tun soll. Es ist vielleicht nicht das, was Sie erwarten oder wollen, aber es funktioniert so. "

7
Aron

Ich bin auf genau dieses Problem gestoßen. Der Haufen ist ziemlich klein, so dass diese Bilder in Bezug auf den Speicher relativ 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 Sie seine Recycle-Methode aufrufen.

Es ist 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 Activity Lifecycle-Diagramm/in der Tabelle auf dieser Seite .

7
Donn Felker

Folgende 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
5
user517491

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

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

5
GSree

Ich schlage einen praktischen 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 beschrieben 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. Aber es hat das Problem nicht vollständig gelöst. Und dann gebe ich die 2. Lösung aus:

Wie das OOM ausführlich beschreibt, habe ich zu viel Laufzeitspeicher verwendet. Also verkleinere 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 von Bildern getan habe, tritt der OOM-Fehler nicht mehr auf.

4
Ranger Way

Auch ich bin frustriert über den Fehler außerhalb des Gedächtnisses. 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 dabei wurde festgestellt, dass sich die Größe meiner App erheblich vergrößert hat. Daher verwende ich jetzt nur ein Bild für alle Dichten und skaliere meine Bilder.

Meine Anwendung hat immer dann einen Fehler ausgegeben, wenn der Benutzer von einer Aktivität zur nächsten gewechselt ist. Das Setzen meiner Drawables auf null und das Aufrufen von System.gc () funktionierten nicht und das Recycling meiner BitmapDrawables mit getBitMap (). Recycle () auch nicht. Android löste den Fehler outofmemory bei der ersten Methode weiterhin aus, und es wurde eine Canvas-Fehlermeldung ausgegeben, wenn bei der zweiten Methode versucht wurde, eine recycelte Bitmap zu verwenden.

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 Methode onPause () tun, sehen Benutzer einen schwarzen Hintergrund. Nicht ideal. Für die onDestroy () -Methode gibt es keine Garantie, dass sie aufgerufen wird.

Um zu verhindern, dass ein schwarzer Bildschirm angezeigt wird, 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 startActivity (getIntent ()) - und dann die finish () -Methode aufrufe.

Hinweis: Der Hintergrund muss nicht unbedingt schwarz sein.

3
subduedjoy

Die BitmapFactory.decode * -Methoden, die in der Lektion Große Bitmaps effizient laden erläutert werden, sollten nicht auf dem Haupt-UI-Thread ausgeführt werden, wenn die Quelldaten von der Festplatte oder einem Netzwerkspeicherort (oder einer anderen Quelle) gelesen werden als Erinnerung). Die Zeit, die diese Daten zum Laden benötigen, ist unvorhersehbar und hängt von einer Reihe von Faktoren ab (Lesegeschwindigkeit von der Festplatte oder vom Netzwerk, Größe des Abbilds, CPU-Leistung 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).

1
Li3ro

FWIW, hier ist ein leichtgewichtiger Bitmap-Cache, den ich codiert und einige Monate lang verwendet habe. Es ist kein Schnickschnack, also lesen Sie 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();
    }

}
0
Guy Smith

Ich hatte das gleiche Problem nur mit dem Wechseln der Hintergrundbilder mit angemessenen Größen. Ich habe bessere Ergebnisse erzielt, wenn ich ImageView auf Null gesetzt habe, bevor ich ein neues Bild einfügte.

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

Nun, ich habe alles ausprobiert, 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 so 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;
    }
}
0
HarryHao