54 votes

Quand dois-je recycler un bitmap en utilisant LRUCache?

Je suis en train d'utiliser un LRUCache pour mettre en cache des bitmaps qui sont stockés sur le système de fichiers. J'ai construit le cache en suivant les exemples ici: http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html

Le problème est que je rencontre fréquemment des plantages "OutOfMemory" lors de l'utilisation de l'application. Je pense que lorsque le LRUCache évacue une image pour faire de la place pour une autre, la mémoire n'est pas libérée.

J'ai ajouté un appel à Bitmap.recycle() lorsque une image est évacuée:

  // utiliser 1/8 de la mémoire disponible pour ce cache de mémoire
    final int cacheSize = 1024 * 1024 * memClass / 8;
                mImageCache = new LruCache(cacheSize) {
                @Override
                protected int sizeOf(String key, Bitmap bitmap) {
                    return bitmap.getByteCount();
                }

                @Override
                protected void entryRemoved(boolean evicted, String key, Bitmap oldBitmap, Bitmap newBitmap) {
                    oldBitmap.recycle();
                    oldBitmap = null;
                }
            };

Cela corrige les plantages, cependant cela entraîne également des images qui n'apparaissent parfois pas dans l'application (juste un espace noir là où l'image devrait être). Chaque fois que cela se produit, je vois ce message dans mon Logcat: Cannot generate texture from bitmap.

Une recherche rapide sur Google révèle que ce problème se produit car l'image affichée a été recyclée.

Alors que se passe-t-il ici? Pourquoi les images recyclées sont-elles encore dans le LRUCache si je ne les recycle qu'après les avoir supprimées? Quelle est l'alternative pour implémenter un cache? Les documents Android indiquent clairement que LRUCache est la voie à suivre, mais ils ne mentionnent pas la nécessité de recycler les bitmaps ni comment le faire.

RÉSOLU: Au cas où cela serait utile à quelqu'un d'autre, la solution à ce problème telle que suggérée par la réponse acceptée est de NE PAS faire ce que j'ai fait dans l'exemple de code ci-dessus (ne pas recycler les bitmaps dans l'appel entryRemoved()).

À la place, lorsque vous avez terminé avec un ImageView (comme dans onPause() dans une activité, ou lorsque une vue est recyclée dans un adaptateur), vérifiez si le bitmap est toujours dans le cache (j'ai ajouté une méthode isImageInCache() à ma classe de cache) et, s'il ne l'est pas, alors recyclez le bitmap. Sinon, laissez-le tel quel. Cela a résolu mes exceptions OutOfMemory et a empêché le recyclage des bitmaps qui étaient encore utilisés.

41voto

CommonsWare Points 402670

Je pense que lorsque le LRUCache évacue une image pour faire de la place pour une autre, la mémoire n'est pas libérée.

Elle ne le sera pas, jusqu'à ce que le Bitmap soit recyclé ou collecté par le garbage collector.

Une rapide recherche sur Google révèle que cela se produit parce que l'image qui est affichée a été recyclée.

C'est pourquoi vous ne devriez pas les recycler là-bas.

Pourquoi les images recyclées sont-elles toujours dans le LRUCache si je les recycle seulement après les avoir supprimées?

Apparemment, elles ne sont pas dans le LRUCache. Elles sont dans un ImageView ou quelque chose d'autre qui utilise toujours le Bitmap.

Quelle est l'alternative pour implémenter un cache?

Pour l'instant, supposons que vous utilisez les objets Bitmap dans des widgets ImageView, comme dans des lignes d'un ListView.

Quand vous avez fini avec un Bitmap (par exemple, une ligne dans un ListView est recyclée), vous vérifiez s'il est toujours dans le cache. Si c'est le cas, vous le laissez tel quel. Sinon, vous l'recyclez().

Le cache vous indique simplement quels objets Bitmap il vaut la peine de conserver. Le cache n'a pas moyen de savoir si le Bitmap est toujours utilisé quelque part.

En passant, si vous êtes sur API Level 11+, envisagez d'utiliser inBitmap. Les erreurs OutOMemory sont déclenchées lorsque qu'une allocation ne peut pas être satisfait. La dernière fois que j'ai vérifié, Android n'a pas de garbage collector compactant, donc vous pouvez obtenir une erreur OutOfMemory en raison de la fragmentation (vouloir allouer quelque chose de plus grand que le plus grand bloc disponible unique).

17voto

Javanator Points 6536

Face à la même et grâce à @CommonsWare pour la discussion. Affichage de la solution complète ici, donc il aide à plus de gens à venir ici pour la même question. Les modifications et Commentaires sont les bienvenus. Cheers

 When should I recycle a bitmap using LRUCache?
  • Précisément lorsque votre image est ni dans le cache et ni se référencés à partir de toute ImageView.

  • Pour maintenir le nombre de références de bitmap, nous devons étendre la BitmapDrawable classe et ajouter les attributs de référence pour eux.

  • Cette android échantillon a la réponse c'est exactement ça. DisplayingBitmaps.zip

Nous allons obtenir les détails et le code ci-dessous.

(don't recycle the bitmaps in the entryRemoved() call).

Pas exactement.

  • Dans entryRemoved délégué vérifier si l'image est toujours référencé à partir de tout ImageView. Si pas. Recycler il lui-même.

  • Et inversement, ce qui est mentionné dans la accepté de répondre que lorsque la vue est sur le point d'obtenir réutilisés ou de se faire plaquer vérifier son image bitmap (précédent bitmap si la vue est réutilisée) est dans le cache. Si c'est là le laisser seul, le reste de la recycler.

  • La clé ici est que nous devons faire vérifier à la fois les lieux de savoir si nous pouvons recycler bitmap ou pas.

Je vais vous expliquer mon cas précis où je suis à l'aide de LruCache de tenir des bitmaps pour moi. Et de les afficher dans la liste. Et l'appel à recycler sur les bitmaps lorsqu'il ne sont plus en cours d'utilisation.

RecyclingBitmapDrawable.java et RecyclingImageView.java de l'exemple mentionné ci-dessus sont les principales pièces dont nous avons besoin ici. Ils sont la manipulation des choses magnifiques. Leur setIsCached et setIsDisplayed méthodes sont en train de faire ce dont nous avons besoin.

Le Code peut être trouvé dans l'échantillon lien mentionné ci-dessus. Mais aussi poster le code complet du fichier dans le fond de la réponse dans le cas où à l'avenir, le lien va vers le bas ou changé. Fait une petite modification de l'annulation de la setImageResource également de vérifier l'état de la précédente image.

- - - - Voici le code pour vous ---

Si votre LruCache gestionnaire devrait ressembler à quelque chose comme ça.

LruCacheManager.java

package com.example.cache;

import android.os.Build;
import android.support.v4.util.LruCache;

public class LruCacheManager {

    private LruCache<String, RecyclingBitmapDrawable> mMemoryCache;

    private static LruCacheManager instance;

    public static LruCacheManager getInstance() {
        if(instance == null) {
            instance = new LruCacheManager();
            instance.init();
        } 

        return instance;
    }

    private void init() {

        // We are declaring a cache of 6Mb for our use.
        // You need to calculate this on the basis of your need 
        mMemoryCache = new LruCache<String, RecyclingBitmapDrawable>(6 * 1024 * 1024) {
            @Override
            protected int sizeOf(String key, RecyclingBitmapDrawable bitmapDrawable) {
                // The cache size will be measured in kilobytes rather than
                // number of items.
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
                    return bitmapDrawable.getBitmap().getByteCount() ;
                } else {
                    return bitmapDrawable.getBitmap().getRowBytes() * bitmapDrawable.getBitmap().getHeight();
                }
            }

            @Override
            protected void entryRemoved(boolean evicted, String key, RecyclingBitmapDrawable oldValue, RecyclingBitmapDrawable newValue) {
                super.entryRemoved(evicted, key, oldValue, newValue);
                oldValue.setIsCached(false);
            }
        };

    }

    public void addBitmapToMemoryCache(String key, RecyclingBitmapDrawable bitmapDrawable) {
        if (getBitmapFromMemCache(key) == null) {
            // The removed entry is a recycling drawable, so notify it
            // that it has been added into the memory cache
            bitmapDrawable.setIsCached(true);
            mMemoryCache.put(key, bitmapDrawable);
        }
    }

    public RecyclingBitmapDrawable getBitmapFromMemCache(String key) {
        return mMemoryCache.get(key);
    }

    public void clear() {
        mMemoryCache.evictAll();
    }
}


Et votre getView() de la ListView/GridView adaptateur doit paraître normal, comme d'habitude. Lorsque vous créez une nouvelle image sur ImageView à l'aide de setImageDrawable méthode. Son interne de vérifier le nombre de références à de précédentes bitmap et fera appel à recycler sur l'interne, si ce n'est dans lrucache.

@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        RecyclingImageView imageView;
        if (convertView == null) { // if it's not recycled, initialize some attributes
            imageView = new RecyclingImageView(getActivity());
            imageView.setLayoutParams(new GridView.LayoutParams(
                    GridView.LayoutParams.WRAP_CONTENT,
                    GridView.LayoutParams.WRAP_CONTENT));
            imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
            imageView.setPadding(5, 5, 5, 5);

        } else {
            imageView = (RecyclingImageView) convertView;
        }

        MyDataObject dataItem = (MyDataObject) getItem(position);
        RecyclingBitmapDrawable  image = lruCacheManager.getBitmapFromMemCache(dataItem.getId());

        if(image != null) {
            // This internally is checking reference count on previous bitmap it used.
            imageView.setImageDrawable(image);
        } else {
            // You have to implement this method as per your code structure.
            // But it basically doing is preparing bitmap in the background
            // and adding that to LruCache.
            // Also it is setting the empty view till bitmap gets loaded.
            // once loaded it just need to call notifyDataSetChanged of adapter. 
            loadImage(dataItem.getId(), R.drawable.empty_view);
        }

        return imageView;

    }

Ici est votre RecyclingImageView.java

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.cache;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.util.AttributeSet;
import android.widget.ImageView;


/**
 * Sub-class of ImageView which automatically notifies the drawable when it is
 * being displayed.
 */
public class RecyclingImageView extends ImageView {

    public RecyclingImageView(Context context) {
        super(context);
    }

    public RecyclingImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * @see android.widget.ImageView#onDetachedFromWindow()
     */
    @Override
    protected void onDetachedFromWindow() {
        // This has been detached from Window, so clear the drawable
        setImageDrawable(null);

        super.onDetachedFromWindow();
    }

    /**
     * @see android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)
     */
    @Override
    public void setImageDrawable(Drawable drawable) {
        // Keep hold of previous Drawable
        final Drawable previousDrawable = getDrawable();

        // Call super to set new Drawable
        super.setImageDrawable(drawable);

        // Notify new Drawable that it is being displayed
        notifyDrawable(drawable, true);

        // Notify old Drawable so it is no longer being displayed
        notifyDrawable(previousDrawable, false);
    }

    /**
     * @see android.widget.ImageView#setImageResource(android.graphics.drawable.Drawable)
     */
    @Override
    public void setImageResource(int resId) {
        // Keep hold of previous Drawable
        final Drawable previousDrawable = getDrawable();

        // Call super to set new Drawable
        super.setImageResource(resId);

        // Notify old Drawable so it is no longer being displayed
        notifyDrawable(previousDrawable, false);
    }


    /**
     * Notifies the drawable that it's displayed state has changed.
     *
     * @param drawable
     * @param isDisplayed
     */
    private static void notifyDrawable(Drawable drawable, final boolean isDisplayed) {
        if (drawable instanceof RecyclingBitmapDrawable) {
            // The drawable is a CountingBitmapDrawable, so notify it
            ((RecyclingBitmapDrawable) drawable).setIsDisplayed(isDisplayed);
        } else if (drawable instanceof LayerDrawable) {
            // The drawable is a LayerDrawable, so recurse on each layer
            LayerDrawable layerDrawable = (LayerDrawable) drawable;
            for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) {
                notifyDrawable(layerDrawable.getDrawable(i), isDisplayed);
            }
        }
    }

}

Ici est votre RecyclingBitmapDrawable.java

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.cache;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;

import android.util.Log;

/**
 * A BitmapDrawable that keeps track of whether it is being displayed or cached.
 * When the drawable is no longer being displayed or cached,
 * {@link android.graphics.Bitmap#recycle() recycle()} will be called on this drawable's bitmap.
 */
public class RecyclingBitmapDrawable extends BitmapDrawable {

    static final String TAG = "CountingBitmapDrawable";

    private int mCacheRefCount = 0;
    private int mDisplayRefCount = 0;

    private boolean mHasBeenDisplayed;

    public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) {
        super(res, bitmap);
    }

    /**
     * Notify the drawable that the displayed state has changed. Internally a
     * count is kept so that the drawable knows when it is no longer being
     * displayed.
     *
     * @param isDisplayed - Whether the drawable is being displayed or not
     */
    public void setIsDisplayed(boolean isDisplayed) {
        //BEGIN_INCLUDE(set_is_displayed)
        synchronized (this) {
            if (isDisplayed) {
                mDisplayRefCount++;
                mHasBeenDisplayed = true;
            } else {
                mDisplayRefCount--;
            }
        }

        // Check to see if recycle() can be called
        checkState();
        //END_INCLUDE(set_is_displayed)
    }

    /**
     * Notify the drawable that the cache state has changed. Internally a count
     * is kept so that the drawable knows when it is no longer being cached.
     *
     * @param isCached - Whether the drawable is being cached or not
     */
    public void setIsCached(boolean isCached) {
        //BEGIN_INCLUDE(set_is_cached)
        synchronized (this) {
            if (isCached) {
                mCacheRefCount++;
            } else {
                mCacheRefCount--;
            }
        }

        // Check to see if recycle() can be called
        checkState();
        //END_INCLUDE(set_is_cached)
    }

    private synchronized void checkState() {
        //BEGIN_INCLUDE(check_state)
        // If the drawable cache and display ref counts = 0, and this drawable
        // has been displayed, then recycle
        if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
                && hasValidBitmap()) {

            Log.d(TAG, "No longer being used or cached so recycling. "
                        + toString());

        getBitmap().recycle();
    }
        //END_INCLUDE(check_state)
    }

    private synchronized boolean hasValidBitmap() {
        Bitmap bitmap = getBitmap();
        return bitmap != null && !bitmap.isRecycled();
    }

}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X