java.lang.OutOfMemoryError: la dimensione della bitmap supera il budget della VM - Android


159

Ho sviluppato un'applicazione che utilizza molte immagini su Android.

L'applicazione viene eseguito una volta, riempie le informazioni sullo schermo ( Layouts, Listviews, Textviews, ImageViews, ecc) e l'utente legge le informazioni.

Non ci sono animazioni, effetti speciali o qualsiasi cosa che possa riempire la memoria. A volte i drawable possono cambiare. Alcuni sono risorse Android e altri sono file salvati in una cartella nella SDCARD.

Quindi l'utente si chiude (il onDestroymetodo viene eseguito e l'app rimane in memoria dalla VM) e quindi a un certo punto l'utente entra di nuovo.

Ogni volta che l'utente accede all'app, posso vedere la memoria crescere sempre di più fino a quando l'utente ottiene il java.lang.OutOfMemoryError.

Quindi qual è il modo migliore / corretto per gestire molte immagini?

Devo metterli in metodi statici in modo che non vengano caricati continuamente? Devo pulire il layout o le immagini utilizzate nel layout in modo speciale?


4
Questo può essere d'aiuto se cambiate molti disegni. Funziona da quando ho fatto il programma da solo :) androidactivity.wordpress.com/2011/09/24/…

Risposte:


72

Sembra che tu abbia una perdita di memoria. Il problema non è gestire molte immagini, è che le tue immagini non vengono deallocate quando la tua attività viene distrutta.

È difficile dire perché questo è senza guardare il tuo codice. Tuttavia, questo articolo contiene alcuni suggerimenti che potrebbero aiutare:

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

In particolare, è probabile che l'uso di variabili statiche peggiori le cose, non migliori. Potrebbe essere necessario aggiungere il codice che rimuove i callback quando l'applicazione viene ridisegnata, ma ancora una volta non ci sono abbastanza informazioni per dirlo con certezza.


8
questo è vero e anche avere molte immagini (e non una perdita di memoria) può causare outOfMemory per diversi motivi come molte altre app sono in esecuzione, frammentazione della memoria, ecc. Ho imparato che lavorando con molte immagini se ottieni outOfMemory sistematicamente, come fare sempre lo stesso, quindi è una perdita. Se lo ottieni una volta al giorno o qualcosa del genere, è perché sei troppo vicino al limite. Il mio suggerimento in questo caso è provare a rimuovere alcune cose e riprovare. Anche la memoria delle immagini è al di fuori della memoria dell'heap, quindi cerca entrambi.
Daniel Benedykt,

15
Se si sospetta una perdita di memoria, un modo rapido per monitorare l'utilizzo dell'heap è tramite il comando meminfo dumpsys shell dumpsys. Se noti un aumento dell'utilizzo dell'heap nel corso di alcuni cicli gc (riga di registro GC_ * in logcat) puoi essere certo di avere una perdita. Quindi crea un dump dell'heap (o più in momenti diversi) tramite adb o DDMS e analizzalo tramite gli strumenti dell'albero del dominatore su Eclipse MAT. Presto dovresti trovare quale oggetto ha un heap di conservazione che cresce nel tempo. Questa è la tua perdita di memoria. Ho scritto un articolo con qualche dettaglio in più qui: macgyverdev.blogspot.com/2011/11/…
Johan Norén,

98

Uno degli errori più comuni che ho riscontrato durante lo sviluppo di app Android è l'errore "java.lang.OutOfMemoryError: la dimensione bitmap supera il budget VM". Ho riscontrato questo errore frequentemente nelle attività che utilizzano molte bitmap dopo aver modificato l'orientamento: l'attività viene distrutta, creata di nuovo e i layout vengono "gonfiati" dall'XML consumando la memoria della VM disponibile per le bitmap.

Le bitmap sul layout dell'attività precedente non vengono correttamente allocate dal Garbage Collector perché hanno incrociato i riferimenti alla loro attività. Dopo molti esperimenti ho trovato una soluzione abbastanza buona per questo problema.

Innanzitutto, imposta l'attributo "id" nella vista padre del layout XML:

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

Quindi, sul onDestroy() metodo della tua attività, chiama il unbindDrawables()metodo passando un riferimento alla vista padre e quindi esegui un 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();
        }
    }

Questo unbindDrawables()metodo esplora l'albero della vista in modo ricorsivo e:

  1. Rimuove i callback su tutti i drawable in background
  2. Rimuove i bambini da ogni viewgroup

10
Tranne ... java.lang.UnsupportedOperationException: removeAllViews () non è supportato in AdapterView

Grazie, questo mi aiuta a ridurre in modo significativo la dimensione del mio heap, dato che ho molti drawable ... @GJTorikian per questo basta provare a rimuovere a AllViews () la maggior parte delle sottoclassi di ViewGroup funzionano bene.
Eric Chen,

5
O semplicemente cambia la condizione: if (visualizza l'istanza di ViewGroup &&! (Visualizza l'istanza di AdapterView))
Anke

2
@Adam Varhegyi, puoi chiamare esplicitamente la stessa funzione per la visualizzazione della galleria in onDestroy. Like unbindDrawables (galleryView); spero che questo
funzioni


11

Per evitare questo problema puoi usare il metodo nativo Bitmap.recycle()prima di null-ing Bitmapobject (o impostare un altro valore). Esempio:

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

E poi puoi cambiare myBitmapsenza chiamare System.gc()come:

setMyBitmap(null);    
setMyBitmap(anotherBitmap);

1
questo non funzionerà se si tenta di aggiungere quegli elementi in una visualizzazione elenco. hai qualche suggerimento al riguardo?
Rafael Sanches,

@ddmytrenko Stai riciclando l'immagine prima di assegnarla. Ciò non impedirebbe il rendering dei suoi pixel?
IgorGanapolsky,

8

Ho riscontrato questo esatto problema. L'heap è piuttosto piccolo, quindi queste immagini possono sfuggire al controllo piuttosto rapidamente per quanto riguarda la memoria. Un modo è quello di dare un suggerimento al garbage collector per raccogliere memoria su una bitmap chiamando il suo metodo di riciclo .

Inoltre, il metodo onDestroy non è garantito per essere chiamato. Potresti voler spostare questa logica / ripulire nell'attività onPause. Consulta il diagramma / tabella del ciclo di vita delle attività in questa pagina per ulteriori informazioni.


Grazie per le informazioni. Sto gestendo tutti i Drawable e non le bitmap, quindi non posso chiamare Recycle. Qualche altro suggerimento per i disegni? Grazie Daniel
Daniel Benedykt il

Grazie per il onPausesuggerimento Sto usando 4 schede con le Bitmapimmagini in tutte, quindi la distruzione dell'attività potrebbe non avvenire troppo presto (se l'utente sfoglia tutte le schede).
Azurespot

7

Questa spiegazione potrebbe aiutare: http://code.google.com/p/android/issues/detail?id=8488#c80

"Suggerimenti rapidi:

1) MAI chiamare System.gc (). Questo è stato propagato come una correzione qui e non funziona. Non farlo. Se hai notato nella mia spiegazione, prima di ottenere un OutOfMemoryError, la JVM esegue già una garbage collection quindi non c'è motivo di eseguirne di nuovo (rallentando il programma). Fare uno alla fine della tua attività è solo nascondere il problema. Potrebbe far sì che la bitmap venga inserita più rapidamente nella coda del finalizzatore, ma non c'è motivo per cui non si possa semplicemente chiamare riciclare su ciascuna bitmap.

2) Chiama sempre recycle () su bitmap che non ti servono più. Per lo meno, in onDestroy della tua attività passa e ricicla tutte le bitmap che stavi utilizzando. Inoltre, se si desidera che le istanze bitmap vengano raccolte dall'heap di dalvik più velocemente, non è male cancellare tutti i riferimenti alla bitmap.

3) Chiamare recycle () e quindi System.gc () potrebbe non essere ancora in grado di rimuovere la bitmap dall'heap di Dalvik. NON ESSERE INTERESSATO per questo. recycle () ha fatto il suo lavoro e liberato la memoria nativa, ci vorrà solo un po 'di tempo per seguire i passaggi che ho descritto in precedenza per rimuovere effettivamente la bitmap dall'heap di Dalvik. Questo NON è un grosso problema perché il grosso pezzo di memoria nativa è già gratuito!

4) Supponi sempre che ci sia un bug nell'ultimo framework. Dalvik sta facendo esattamente quello che dovrebbe fare. Potrebbe non essere quello che ti aspetti o quello che vuoi, ma è come funziona. "


5

Ho avuto lo stesso identico problema. Dopo alcuni test ho scoperto che questo errore appare per il ridimensionamento delle immagini di grandi dimensioni. Ho ridotto il ridimensionamento dell'immagine e il problema è scomparso.

PS All'inizio ho provato a ridurre le dimensioni dell'immagine senza ridimensionare l'immagine. Ciò non ha fermato l'errore.


4
potresti pubblicare il codice su come hai fatto il ridimensionamento delle immagini, sto affrontando lo stesso problema e penso che questo potrebbe risolverlo. Grazie!
Gligor,

@Gix - Credo che significhi che ha dovuto ridurre la dimensione delle immagini prima di portarle nelle risorse del progetto, riducendo così l'impronta di memoria dei disegni. Per questo dovresti usare l'editor fotografico di tua scelta. Mi piace pixlr.com
Kyle Clegg il

5

I seguenti punti mi hanno davvero aiutato molto. Potrebbero esserci anche altri punti, ma questi sono molto cruciali:

  1. Utilizzare il contesto dell'applicazione (invece di activity.this) ove possibile.
  2. Ferma e rilascia i thread nel metodo di attività onPause ()
  3. Rilascia le tue visualizzazioni / callback nel metodo di attività onDestroy ()

4

Suggerisco un modo conveniente per risolvere questo problema. Basta assegnare l'attributo "android: configChanges" come segue in Mainfest.xml per la tua attività errata. come questo:

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

la prima soluzione che ho dato aveva davvero ridotto la frequenza dell'errore OOM a un livello basso. Ma non ha risolto completamente il problema. E poi darò la seconda soluzione:

Come dettagliato in OOM, ho usato troppa memoria di runtime. Quindi, riduco le dimensioni dell'immagine in ~ / res / drawable del mio progetto. Ad esempio un'immagine sovraqualificata che ha una risoluzione di 128X128, potrebbe essere ridimensionata a 64x64, che sarebbe adatta anche per la mia applicazione. E dopo averlo fatto con una pila di immagini, l'errore OOM non si verifica più.


1
Questo funziona perché stai evitando il riavvio dell'app. Quindi non è tanto una soluzione quanto un elusione. Ma a volte è tutto ciò di cui hai bisogno. E il metodo onConfigurationChanged () predefinito in Attività cambierà l'orientamento per te, quindi se non hai bisogno di modifiche all'interfaccia utente per adattarti veramente ai diversi orientamenti, potresti essere perfettamente soddisfatto del risultato. Fai attenzione ad altre cose che possono riavviarti, però. Potresti voler usare questo: android: configChanges = "keyboardHidden | orientamento | tastiera | locale | mcc | mnc | touchscreen | screenLayout | fontScale"
Carl

3

Anch'io sono frustrato dal bug outofmemory. E sì, anch'io ho scoperto che questo errore si presenta molto durante il ridimensionamento delle immagini. All'inizio ho provato a creare dimensioni di immagine per tutte le densità, ma ho scoperto che ciò ha aumentato notevolmente le dimensioni della mia app. Quindi ora sto solo usando un'immagine per tutte le densità e ridimensionando le mie immagini.

La mia applicazione genererebbe un errore anomalo ogni volta che l'utente passava da un'attività all'altra. Impostare i miei drawable su null e chiamare System.gc () non ha funzionato, né riciclare i miei bitmapDrawables con getBitMap (). Recycle (). Android continuerebbe a lanciare l'errore outofmemory con il primo approccio e genererebbe un messaggio di errore canvas ogni volta che tenta di utilizzare una bitmap riciclata con il secondo approccio.

Ho adottato un terzo approccio. Ho impostato tutte le viste su null e lo sfondo su nero. Faccio questa pulizia nel mio metodo onStop (). Questo è il metodo che viene chiamato non appena l'attività non è più visibile. Se lo fai con il metodo onPause (), gli utenti vedranno uno sfondo nero. Non ideale Per quanto riguarda farlo nel metodo onDestroy (), non vi è alcuna garanzia che verrà chiamato.

Per evitare che si verifichi una schermata nera se l'utente preme il pulsante Indietro sul dispositivo, ricarico l'attività nel metodo onRestart () chiamando i metodi startActivity (getIntent ()) e quindi finish ().

Nota: non è davvero necessario cambiare lo sfondo in nero.


1

I metodi BitmapFactory.decode *, discussi nella lezione Carica grandi bitmap in modo efficiente , non dovrebbero essere eseguiti sul thread dell'interfaccia utente principale se i dati di origine vengono letti dal disco o da un percorso di rete (o in realtà qualsiasi fonte diversa dalla memoria). Il tempo necessario per caricare questi dati è imprevedibile e dipende da una varietà di fattori (velocità di lettura da disco o rete, dimensione dell'immagine, potenza della CPU, ecc.). Se una di queste attività blocca il thread dell'interfaccia utente, il sistema contrassegna l'applicazione come non reattiva e l'utente ha la possibilità di chiuderla (per ulteriori informazioni, consultare Progettazione di reattività).


0

Beh, ho provato tutto quello che ho trovato su Internet e nessuno di loro ha funzionato. La chiamata a System.gc () riduce solo la velocità dell'app. Il riciclaggio delle bitmap in onDestroy non ha funzionato anche per me.

L'unica cosa che funziona ora è avere un elenco statico di tutte le bitmap in modo che le bitmap sopravvivano dopo un riavvio. E basta usare le bitmap salvate invece di crearne di nuove ogni volta che l'attività viene riavviata.

Nel mio caso il codice è simile al seguente:

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;
    }
}

questo non colpirà il tuo contesto? (dati relativi all'attività)
Rafael Sanches,

0

Ho avuto lo stesso problema solo cambiando le immagini di sfondo con dimensioni ragionevoli. Ho ottenuto risultati migliori impostando ImageView su null prima di inserire una nuova immagine.

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

0

FWIW, ecco una cache bitmap leggera che ho codificato e che ho usato per alcuni mesi. Non sono tutte le campane e fischietti, quindi leggi il codice prima di usarlo.

/**
 * 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();
    }

}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.