java.lang.OutOfMemoryError: la taille du bitmap dépasse le budget de la VM - Android

159

J'ai développé une application qui utilise beaucoup d'images sur Android.

L'application exécute une fois, remplit les informations sur l'écran ( Layouts, Listviews, Textviews, ImageViews, etc.) et l' utilisateur lit les informations.

Il n'y a pas d'animation, pas d'effets spéciaux ou quoi que ce soit qui puisse remplir la mémoire. Parfois, les tirages peuvent changer. Certaines sont des ressources Android et d'autres sont des fichiers enregistrés dans un dossier de la SDCARD.

Ensuite, l'utilisateur quitte (la onDestroyméthode est exécutée et l'application reste en mémoire par la machine virtuelle), puis à un moment donné, l'utilisateur entre à nouveau.

Chaque fois que l'utilisateur entre dans l'application, je peux voir la mémoire augmenter de plus en plus jusqu'à ce que l'utilisateur obtienne le fichier java.lang.OutOfMemoryError.

Alors, quelle est la meilleure / correcte façon de gérer de nombreuses images?

Dois-je les mettre dans des méthodes statiques pour qu'elles ne soient pas chargées tout le temps? Dois-je nettoyer la mise en page ou les images utilisées dans la mise en page d'une manière spéciale?

Daniel Benedykt
la source
4
Cela peut vous aider si vous avez beaucoup de dessinables à changer. Cela fonctionne depuis que j'ai fait le programme moi-même :) androidactivity.wordpress.com/2011/09/24/…

Réponses:

72

Il semble que vous ayez une fuite de mémoire. Le problème n'est pas de gérer beaucoup d'images, c'est que vos images ne sont pas désallouées lorsque votre activité est détruite.

Il est difficile de dire pourquoi sans regarder votre code. Cependant, cet article contient quelques conseils qui pourraient vous aider:

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

En particulier, l'utilisation de variables statiques risque d'aggraver les choses, pas de les améliorer. Vous devrez peut-être ajouter un code qui supprime les rappels lorsque votre application est redessinée - mais encore une fois, il n'y a pas suffisamment d'informations ici pour être sûr.

Trevor Johns
la source
8
c'est vrai et aussi avoir beaucoup d'images (et pas une fuite de mémoire) peut causer outOfMemory pour plusieurs raisons comme beaucoup d'autres applications sont en cours d'exécution, la fragmentation de la mémoire, etc. systématiquement, comme faire toujours la même chose, alors c'est une fuite. Si vous l'obtenez une fois par jour ou quelque chose du genre, c'est parce que vous êtes trop près de la limite. Ma suggestion dans ce cas est d'essayer de supprimer des éléments et de réessayer. De plus, la mémoire d'image est en dehors de la mémoire de tas, alors faites attention aux deux.
Daniel Benedykt
15
Si vous suspectez une fuite de mémoire, un moyen rapide de surveiller l'utilisation du tas consiste à utiliser la commande adb shell dumpsys meminfo. Si vous voyez l'utilisation du tas augmenter sur quelques cycles gc (ligne de journal GC_ * dans logcat), vous pouvez être presque sûr d'avoir une fuite. Créez ensuite un heap dump (ou plusieurs à des moments différents) via adb ou DDMS et analysez-le via l'outillage de l'arbre dominateur sur Eclipse MAT. Vous devriez bientôt trouver quel objet a un tas de rétines qui augmente avec le temps. C'est votre fuite de mémoire. J'ai écrit un article avec plus de détails ici: macgyverdev.blogspot.com/2011/11/…
Johan Norén
98

L'une des erreurs les plus courantes que j'ai trouvées lors du développement d'applications Android est l'erreur «java.lang.OutOfMemoryError: Bitmap Size Exceeds VM Budget». J'ai trouvé cette erreur fréquemment sur les activités utilisant beaucoup de bitmaps après avoir changé d'orientation: l'activité est détruite, créée à nouveau et les mises en page sont «gonflées» à partir du XML consommant la mémoire VM disponible pour les bitmaps.

Les bitmaps de la disposition d'activité précédente ne sont pas correctement désalloués par le garbage collector car ils ont croisé des références à leur activité. Après de nombreuses expériences, j'ai trouvé une assez bonne solution à ce problème.

Tout d'abord, définissez l'attribut "id" sur la vue parente de votre mise en page 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"
     >
     ...

Ensuite, sur la onDestroy() méthode de votre activité, appelez la unbindDrawables()méthode en passant une référence à la vue parent, puis effectuez 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();
        }
    }

Cette unbindDrawables()méthode explore l'arborescence des vues de manière récursive et:

  1. Supprime les rappels sur tous les drawables d'arrière-plan
  2. Supprime les enfants dans chaque groupe de vues
hp.android
la source
10
Sauf ... java.lang.UnsupportedOperationException: removeAllViews () n'est pas pris en charge dans AdapterView
Merci, cela aide à réduire considérablement la taille de mon tas car j'ai beaucoup de drawables ... @GJTorikian pour cela, essayez simplement catch à removeAllViews () la plupart des sous-classes de ViewGroup fonctionnent très bien.
Eric Chen du
5
Ou modifiez simplement la condition: if (view instanceof ViewGroup &&! (View instanceof AdapterView))
Anke
2
@Adam Varhegyi, vous pouvez appeler explicitement la même fonction pour la vue Galerie dans onDestroy. Comme unbindDrawables (galleryView); j'espère que cela fonctionne pour vous ...
hp.android
11

Pour éviter ce problème, vous pouvez utiliser la méthode native Bitmap.recycle()avant null-ing Bitmapobject (ou définir une autre valeur). Exemple:

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

Et ensuite tu peux changer myBitmap sans appeler System.gc()comme:

setMyBitmap(null);    
setMyBitmap(anotherBitmap);
ddmytrenko
la source
1
cela ne fonctionnera pas si vous essayez d'ajouter ces éléments dans une liste. avez-vous des suggestions à ce sujet?
Rafael Sanches
@ddmytrenko Vous recyclez l'image avant de l'attribuer. Cela n'empêcherait-il pas ses pixels de se rendre?
IgorGanapolsky
8

J'ai rencontré ce problème exact. Le tas est assez petit, donc ces images peuvent devenir incontrôlables assez rapidement en ce qui concerne la mémoire. Une façon consiste à donner au ramasse-miettes un indice pour collecter de la mémoire sur un bitmap en appelant sa méthode de recyclage .

En outre, la méthode onDestroy n'est pas garantie d'être appelée. Vous souhaiterez peut-être déplacer cette logique / nettoyage dans l'activité onPause. Consultez le diagramme / tableau du cycle de vie des activités sur cette page pour plus d'informations.

Donn Felker
la source
Merci pour l'info. Je gère tous les Drawables et non les Bitmaps, donc je ne peux pas appeler Recycle. Une autre suggestion pour les drawables? Merci Daniel
Daniel Benedykt
Merci pour la onPausesuggestion. J'utilise 4 onglets avec des Bitmapimages dans tous, donc la destruction de l'activité peut ne pas se produire trop tôt (si l'utilisateur parcourt tous les onglets).
Azurespot
7

Cette explication peut vous aider: http://code.google.com/p/android/issues/detail?id=8488#c80

"Astuces rapides:

1) N'appelez JAMAIS System.gc () vous-même. Cela a été propagé comme un correctif ici, et cela ne fonctionne pas. Ne fais pas ça. Si vous avez remarqué dans mon explication, avant d'obtenir une OutOfMemoryError, la JVM exécute déjà un garbage collection donc il n'y a aucune raison d'en refaire un (cela ralentit votre programme). Faire un à la fin de votre activité ne fait que dissimuler le problème. Cela peut accélérer le placement du bitmap dans la file d'attente du finaliseur, mais il n'y a aucune raison pour laquelle vous n'auriez pas pu simplement appeler recycle sur chaque bitmap.

2) Appelez toujours recycle () sur les bitmaps dont vous n'avez plus besoin. À tout le moins, dans le onDestroy de votre activité, parcourez et recyclez tous les bitmaps que vous utilisiez. De plus, si vous voulez que les instances bitmap soient collectées plus rapidement à partir du tas dalvik, cela ne fait pas de mal d'effacer les références au bitmap.

3) L'appel de recycle () puis de System.gc () risque de ne pas supprimer le bitmap du tas Dalvik. Ne vous inquiétez pas de cela. recycle () a fait son travail et a libéré la mémoire native, il faudra juste un certain temps pour suivre les étapes que j'ai décrites plus tôt pour supprimer le bitmap du tas Dalvik. Ce n'est PAS un gros problème car la grande partie de la mémoire native est déjà libre!

4) Supposons toujours qu'il y ait un bogue dans le framework en dernier. Dalvik fait exactement ce qu'il est censé faire. Ce n'est peut-être pas ce que vous attendez ou ce que vous voulez, mais c'est ainsi que cela fonctionne. "

Aron
la source
5

J'ai eu exactement le même problème. Après quelques tests, j'ai constaté que cette erreur apparaît pour la mise à l'échelle de grandes images. J'ai réduit la mise à l'échelle de l'image et le problème a disparu.

PS Au début, j'ai essayé de réduire la taille de l'image sans réduire la taille de l'image. Cela n'a pas arrêté l'erreur.

GSree
la source
4
pourriez-vous poster le code sur la façon dont vous avez fait la mise à l'échelle de l'image, je suis confronté au même problème et je pense que cela pourrait le résoudre. Merci!
Gligor
@Gix - Je crois qu'il veut dire qu'il a dû réduire la taille des images avant de les intégrer dans les ressources du projet, réduisant ainsi l'empreinte mémoire des drawables. Pour cela, vous devez utiliser l'éditeur de photos de votre choix. J'aime pixlr.com
Kyle Clegg
5

Les points suivants m'ont vraiment beaucoup aidé. Il peut y avoir d'autres points aussi, mais ceux-ci sont très cruciaux:

  1. Utilisez le contexte d'application (au lieu de activity.this) dans la mesure du possible.
  2. Arrêtez et relâchez vos threads dans la méthode d'activité onPause ()
  3. Libérez vos vues / rappels dans la méthode d'activité onDestroy ()

la source
4

Je suggère un moyen pratique de résoudre ce problème. Attribuez simplement la valeur de l'attribut "android: configChanges" comme suit dans Mainfest.xml pour votre activité erronée. comme ça:

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

la première solution que j'ai donnée avait vraiment réduit la fréquence des erreurs OOM à un niveau bas. Mais cela n'a pas totalement résolu le problème. Et puis je donnerai la 2ème solution:

Comme le MOO l'a détaillé, j'ai utilisé trop de mémoire d'exécution. Donc, je réduis la taille de l'image en ~ / res / drawable de mon projet. Comme une image surqualifiée qui a une résolution de 128X128, pourrait être redimensionnée à 64x64, ce qui conviendrait également à mon application. Et après l'avoir fait avec une pile d'images, l'erreur MOO ne se reproduit plus.

Chemin Ranger
la source
1
Cela fonctionne car vous évitez le redémarrage de votre application. Ce n'est donc pas tant une solution qu'un évitement. Mais parfois, c'est tout ce dont vous avez besoin. Et la méthode onConfigurationChanged () par défaut dans Activity inversera votre orientation pour vous, donc si vous n'avez pas besoin de modifications d'interface utilisateur pour vraiment vous adapter aux différentes orientations, vous serez peut-être parfaitement satisfait du résultat. Méfiez-vous des autres choses qui peuvent vous redémarrer, cependant. Vous pouvez utiliser ceci: android: configChanges = "keyboardHidden | orientation | keyboard | locale | mcc | mnc | touchscreen | screenLayout | fontScale"
Carl
3

Je suis moi aussi frustré par le bug de la mémoire. Et oui, j'ai moi aussi trouvé que cette erreur apparaît souvent lors de la mise à l'échelle des images. Au début, j'ai essayé de créer des tailles d'image pour toutes les densités, mais j'ai trouvé que cela augmentait considérablement la taille de mon application. Je n'utilise donc plus qu'une seule image pour toutes les densités et la mise à l'échelle de mes images.

Mon application lèverait une erreur de mémoire excessive chaque fois que l'utilisateur passait d'une activité à une autre. Définir mes drawables sur null et appeler System.gc () ne fonctionnait pas, ni recycler mes bitmapDrawables avec getBitMap (). Recycle (). Android continuerait à lancer l'erreur de mémoire morte avec la première approche, et il lancerait un message d'erreur de canevas chaque fois qu'il essayait d'utiliser une image bitmap recyclée avec la deuxième approche.

J'ai adopté une troisième approche. J'ai défini toutes les vues sur nulles et l'arrière-plan sur noir. Je fais ce nettoyage dans ma méthode onStop (). C'est la méthode qui est appelée dès que l'activité n'est plus visible. Si vous le faites dans la méthode onPause (), les utilisateurs verront un fond noir. Pas idéal. Quant à le faire dans la méthode onDestroy (), il n'y a aucune garantie qu'il sera appelé.

Pour éviter qu'un écran noir ne se produise si l'utilisateur appuie sur le bouton retour de l'appareil, je recharge l'activité dans la méthode onRestart () en appelant les méthodes startActivity (getIntent ()) puis finish ().

Remarque: il n'est pas vraiment nécessaire de changer l'arrière-plan en noir.

joie douce
la source
1

Les méthodes BitmapFactory.decode *, abordées dans la leçon Charger efficacement de gros bitmaps , ne doivent pas être exécutées sur le thread principal de l'interface utilisateur si les données source sont lues à partir du disque ou d'un emplacement réseau (ou en fait de toute source autre que la mémoire). Le temps de chargement de ces données est imprévisible et dépend de divers facteurs (vitesse de lecture à partir du disque ou du réseau, taille de l'image, puissance du processeur, etc.). Si l'une de ces tâches bloque le thread d'interface utilisateur, le système marque votre application comme non réactive et l'utilisateur a la possibilité de la fermer (voir Conception pour la réactivité pour plus d'informations).

Li3ro
la source
0

Eh bien, j'ai essayé tout ce que j'ai trouvé sur Internet et aucun d'entre eux n'a fonctionné. L'appel de System.gc () ne fait que ralentir la vitesse de l'application. Le recyclage des bitmaps dans onDestroy ne fonctionnait pas non plus pour moi.

La seule chose qui fonctionne maintenant est d'avoir une liste statique de tous les bitmap afin que les bitmaps survivent après un redémarrage. Et utilisez simplement les bitmaps enregistrés au lieu d'en créer de nouveaux à chaque redémarrage de l'activité.

Dans mon cas, le code ressemble à ceci:

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;
    }
}
HarryHao
la source
cela ne ferait-il pas fuir votre contexte? (données d'activité)
Rafael Sanches
0

J'ai eu le même problème avec la commutation des images d'arrière-plan avec des tailles raisonnables. J'ai obtenu de meilleurs résultats en définissant ImageView sur null avant de mettre une nouvelle image.

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

FWIW, voici un cache bitmap léger que j'ai codé et utilisé depuis quelques mois. Ce n'est pas tout-les-cloches-et-sifflets, alors lisez le code avant de l'utiliser.

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

}
Guy Smith
la source