web-dev-qa-db-fra.com

Quand dois-je recycler un bitmap à l'aide de LRUCache?

J'utilise un LRUCache pour mettre en cache les bitmaps stockés sur le système de fichiers. J'ai construit le cache sur la base des exemples ici: http://developer.Android.com/training/displaying-bitmaps/cache-bitmap.html

Le problème est que je vois OutOfMemory se bloquer fréquemment lors de l'utilisation de l'application. Je crois que lorsque le LRUCache expulse une image pour faire place à une autre, la mémoire n'est pas libérée.

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

  // use 1/8 of the available memory for this memory cache
    final int cacheSize = 1024 * 1024 * memClass / 8;
                mImageCache = new LruCache<String, Bitmap>(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, mais cela entraîne également des images parfois n'apparaissant pas dans l'application (juste un espace noir 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 cela se produit car l'image qui s'affiche a été recyclée.

Alors qu'est-ce qui se passe ici? Pourquoi les images recyclées sont-elles toujours dans le LRUCache si je ne les recycle qu'après leur retrait? Quelle est l'alternative pour implémenter un cache? Les documents Android Android indiquent clairement que LRUCache est le chemin à parcourir, mais ils ne mentionnent pas la nécessité de recycler les bitmaps ou comment le faire.

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

Au lieu de cela, lorsque vous avez terminé avec une ImageView (telle que onPause() dans une activité, ou lorsqu'une vue est recyclée dans un adaptateur), vérifiez si le bitmap est toujours dans le cache (j'ai ajouté une fonction isImageInCache() méthode à ma classe de cache) et, si ce n'est pas le cas, recyclez le bitmap. Sinon, laissez-le tranquille. Cela a corrigé mes OutOfMemory exceptions et a empêché le recyclage des bitmaps qui étaient encore utilisés.

54
howettl

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

Ce ne sera pas le cas tant que le Bitmap ne sera pas recyclé ou récupéré.

Une recherche rapide sur Google révèle que cela se produit car l'image qui s'affiche a été recyclée.

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

Pourquoi les images recyclées sont-elles toujours dans le LRUCache si je ne les recycle qu'après leur retrait?

Vraisemblablement, ils ne sont pas dans le LRUCache. Ils sont dans un ImageView ou quelque chose d'autre qui utilise toujours le Bitmap.

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

Par souci d'argument, supposons que vous utilisez les objets Bitmap dans les widgets ImageView, comme dans les lignes d'un ListView.

Lorsque vous avez terminé avec un Bitmap (par exemple, une ligne dans un ListView est recyclée), vous vérifiez si elle est toujours dans le cache. Si c'est le cas, vous le laissez tranquille. Si ce n'est pas le cas, vous recycle().

Le cache vous permet simplement de savoir quels objets Bitmap méritent d'être conservés. Le cache n'a aucun moyen de savoir si le Bitmap est toujours utilisé quelque part.

BTW, si vous êtes sur l'API niveau 11+, pensez à utiliser inBitmap . OutOMemoryErrors sont déclenchés lorsqu'une allocation ne peut pas être remplie. La dernière fois que j'ai vérifié, Android n'a pas de ramasse-miettes de compactage, vous pouvez donc obtenir un OutOfMemoryError en raison de la fragmentation (vous voulez allouer quelque chose de plus grand que le plus gros bloc disponible).

41
CommonsWare

Face à la même chose et merci à @CommonsWare pour la discussion. Publier la solution complète ici afin d'aider plus de personnes à venir ici pour le même problème. Les modifications et commentaires sont les bienvenus. À votre santé

 When should I recycle a bitmap using LRUCache?
  • Précisément lorsque votre Bitmap n'est ni en cache ni référencé à partir d'une ImageView.

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

  • Cet Android a la réponse exactement. DisplayingBitmaps.Zip

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

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

Pas exactement.

  • Dans le délégué d'entrée supprimé, vérifiez si Bitmap est toujours référencé à partir d'une ImageView. Si non. Recyclez-le là-bas.

  • Et vice versa, qui est mentionné dans la réponse acceptée selon laquelle lorsque la vue est sur le point d'être réutilisée ou d'être vidée, vérifiez que son bitmap (bitmap précédent si la vue est réutilisée) est dans le cache. S'il est là, laissez-le tranquille, recyclez-le.

  • La clé ici est que nous devons vérifier aux deux endroits si nous pouvons recycler le bitmap ou non.

Je vais expliquer mon cas spécifique où j'utilise LruCache pour conserver des bitmaps pour moi. Et les afficher dans ListView. Et appeler recycle sur les bitmaps lorsqu'ils ne sont plus utilisés.

RecyclingBitmapDrawable.Java et RecyclingImageView.Java de l'échantillon mentionné ci-dessus sont les pièces de base dont nous avons besoin ici. Ils gèrent très bien les choses. Leur setIsCached et setIsDisplayed les méthodes font ce dont nous avons besoin.

Le code peut être trouvé dans l'exemple de lien mentionné ci-dessus. Mais aussi en affichant le code complet du fichier en bas de la réponse au cas où à l'avenir le lien tomberait ou changerait. A fait une petite modification du remplacement de setImageResource également pour vérifier l'état du bitmap précédent.

--- Voici le code pour vous ---

Votre gestionnaire LruCache devrait donc ressembler à ceci.

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 ton getView () de l'adaptateur ListView/GridView devrait sembler normal comme d'habitude. Comme lorsque vous définissez une nouvelle image sur ImageView à l'aide de la méthode setImageDrawable. Il vérifie en interne le nombre de références sur le bitmap précédent et appellera recycle dessus s'il n'est pas 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;

    }

Voici ton 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);
            }
        }
    }

}

Voici ton 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();
    }

}
17
Javanator