web-dev-qa-db-fra.com

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

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

L'application s'exécute une fois, remplit les informations à l'écran (Layouts, Listviews, Textviews, ImageViews, etc.) et l'utilisateur lit ces 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 tirables peuvent changer. Certaines sont des ressources Android) et d’autres sont des fichiers enregistrés dans un dossier de la carte SDCARD.

Puis l'utilisateur se ferme (la méthode onDestroy est exécutée et l'application reste en mémoire de la part de VM)), puis à nouveau à un moment donné.

Chaque fois que l'utilisateur entre dans l'application, la mémoire augmente de plus en plus jusqu'à ce que l'utilisateur obtienne le Java.lang.OutOfMemoryError.

Alors, quel est le meilleur/correct moyen de gérer de nombreuses images?

Devrais-je les mettre dans des méthodes statiques afin qu'ils ne soient pas chargés 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?

157
Daniel Benedykt

On dirait que vous avez 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 c'est sans regarder votre code. Cependant, cet article contient des conseils utiles:

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 du code qui supprime les rappels lorsque votre application se redessine - mais encore une fois, il n'y a pas assez d'informations ici pour vous en assurer.

70
Trevor Johns

L'une des erreurs les plus courantes que j'ai rencontrées lors du développement Android Apps est l'erreur "Java.lang.OutOfMemoryError: la taille du bitmap dépasse la valeur VM Budget"). J'ai trouvé ceci. erreur fréquemment lors d'activités utilisant beaucoup de bitmaps après avoir changé d'orientation: l'activité est détruite, créée à nouveau et les présentations sont "gonflées" à partir du XML qui utilise la mémoire VM disponible pour les bitmaps.

Les bitmaps de la structure d'activité précédente ne sont pas correctement désalloués par le récupérateur de mémoire, car ils ont des références croisées avec leur activité. Après de nombreuses expériences, j'ai trouvé une très bonne solution à ce problème.

Tout d’abord, définissez l’attribut "id" sur la vue parent de votre présentation 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, dans la méthode onDestroy() de votre activité, appelez la méthode unbindDrawables() en transmettant une référence à la vue parent, puis effectuez une opération 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 méthode unbindDrawables() explore l'arborescence de la vue de manière récursive et:

  1. Supprime les rappels sur tous les éléments dessinables en arrière-plan
  2. Supprime les enfants sur chaque groupe de vues
96
hp.android

Pour éviter ce problème, vous pouvez utiliser la méthode native Bitmap.recycle() avant nulling Bitmap object (ou en définissant une autre valeur). Exemple:

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

Et ensuite, vous pouvez changer myBitmap sans appeler System.gc() comme:

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

Cette explication pourrait aider: http://code.google.com/p/Android/issues/detail?id=8488#c8

"Conseils rapides:

1) NE JAMAIS appeler System.gc () vous-même. Cela a été propagé comme une solution ici, et cela ne fonctionne pas. Ne fais pas ça. Si vous avez remarqué dans mon explication, avant d'obtenir une erreur OutOfMemoryError, la machine virtuelle Java exécute déjà un garbage collection. Il n'y a donc aucune raison de le refaire (cela ralentit votre programme). En faire une à la fin de votre activité ne fait que masquer le problème. Cela peut entraîner une mise plus rapide de la bitmap dans la file d'attente du finaliseur, mais rien ne vous empêche de simplement appeler recycler sur chaque bitmap.

2) Appelez toujours recycle () sur les bitmaps dont vous n’avez plus besoin. À tout le moins, dans le programme OnDestroy de votre activité, recyclez et recyclez toutes les images bitmap que vous utilisiez. En outre, si vous souhaitez que les occurrences de bitmap soient collectées plus rapidement à partir du tas de dalvik, effacez toutes les références au bitmap.

3) L'appel de recycle (), puis System.gc () peut toujours ne pas supprimer le bitmap du tas de Dalvik. Ne vous inquiétez pas à ce sujet. recycle () a fait son travail et a libéré la mémoire native. Il vous faudra un peu de temps pour passer en revue les étapes décrites précédemment afin de supprimer le bitmap du tas de Dalvik. Ce n'est pas un gros problème car la grande quantité de mémoire native est déjà libre!

4) Supposons toujours qu’il ya un bogue dans le framework en dernier. Dalvik fait exactement ce que son supposé faire. Ce n'est peut-être pas ce que vous attendez ou ce que vous voulez, mais sa façon de fonctionner. "

7
Aron

J'ai rencontré ce problème précis. Le tas étant assez petit, ces images peuvent rapidement perdre le contrôle de la mémoire. Une solution consiste à indiquer au récupérateur de mémoire de collecter de la mémoire sur une image bitmap en appelant sa méthode de recyclage .

En outre, il n'est pas garanti que la méthode onDestroy soit appelée. Vous voudrez 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.

7
Donn Felker

Les points suivants m'ont beaucoup aidé. Il y a peut-être d'autres points également, mais ils sont très importants:

  1. Utilisez le contexte de l'application (au lieu de activity.this) chaque fois que possible.
  2. Arrêtez et relâchez vos discussions dans la méthode d'activité onPause ()
  3. Libérez vos vues/rappels dans la méthode d'activité onDestroy ()
5
user517491

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

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

5
GSree

Je suggère un moyen pratique de résoudre ce problème. Attribuez simplement la valeur d'attribut "Android: configChanges" comme indiqué dans le fichier 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’avais donnée avait réellement réduit la fréquence des erreurs de MOO à un niveau bas. Mais cela n'a pas résolu le problème totalement. Et puis je donnerai la 2ème solution:

En tant que MOO détaillé, j'ai utilisé trop de mémoire d'exécution. Donc, je réduis la taille de l'image en ~/res/dessinable de mon projet. Une image surqualifiée, avec 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 de photos, l'erreur de MOO ne se reproduit plus.

4
Ranger Way

Moi aussi, je suis frustré par le bug de mémoire obsolète. Et oui, j'ai aussi constaté que cette erreur apparaissait 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 constaté que cela augmentait considérablement la taille de mon application. J'utilise donc à présent une seule image pour toutes les densités et redimensionne mes images.

Mon application générait une erreur de mémoire superficielle chaque fois que l'utilisateur passait d'une activité à une autre. La définition de nullable de mes éléments dessinables et l'appel de System.gc () n'ont pas fonctionné, pas plus que le recyclage de mes bitmapDrawables avec getBitMap (). Recycle (). Android continuerait à émettre l'erreur de mémoire sortante avec la première approche, et un message d'erreur de type toile s'afficherait chaque fois qu'il tenterait d'utiliser un bitmap recyclé avec la deuxième approche.

J'ai même pris une troisième approche. Je règle toutes les vues sur null 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. Pour ce qui est de le faire dans la méthode onDestroy (), rien ne garantit qu’il sera appelé.

Pour éviter qu'un écran noir ne se produise si l'utilisateur appuie sur le bouton Précédent 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 le fond en noir.

3
subduedjoy

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

1
Li3ro

FWIW, voici un cache bitmap léger que j'ai codé et utilisé pendant quelques mois. Ce n'est pas tout-à-fait-et-des-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();
    }

}
0
Guy Smith

J'ai eu le même problème juste 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 placer une nouvelle image.

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

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 d'application. Recycler des bitmaps dans onDestroy n'a pas fonctionné pour moi aussi.

La seule chose qui fonctionne maintenant est d'avoir une liste statique de tout le 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 fois que l'activité est redémarrée.

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