When should I recycle a bitmap using LRUCache?
Asked Answered
U

2

56

I'm using an LRUCache to cache bitmaps which are stored on the file system. I built the cache based on the examples here: http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html

The problem is that I'm seeing OutOfMemory crashes frequently while using the app. I believe that when the LRUCache evicts an image to make room for another one, the memory is not being freed.

I added a call to Bitmap.recycle() when an image is evicted:

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

This fixes the crashes, however it also results in images sometimes not appearing in the app (just a black space where the image should be). Any time that occurs I see this message in my Logcat: Cannot generate texture from bitmap.

A quick google search reveals that this is happening because the image which is displaying has been recycled.

So what is happening here? Why are recycled images still in the LRUCache if I'm only recycling them after they've been removed? What is the alternative for implementing a cache? The Android docs clearly state that LRUCache is the way to go, but they do not mention the need to recycle bitmaps or how to do so.

RESOLVED: In case its useful to anyone else, the solution to this problem as suggested by the accepted answer is to NOT do what I did in the code example above (don't recycle the bitmaps in the entryRemoved() call).

Instead, when you're finished with an ImageView (such as onPause() in an activity, or when a view is recycled in an adapter) check if the bitmap is still in the cache (I added a isImageInCache() method to my cache class) and, if it's not, then recycle the bitmap. Otherwise, leave it alone. This fixed my OutOfMemory exceptions and prevented recycling bitmaps which were still being used.

Ulterior answered 24/5, 2012 at 18:39 Comment(2)
How are you checking if the bitmap is still in the cache?Jurgen
You can check if image is cached by using ImageLoader like imageLoader.isCached(url, maxWidth, maxHeight); maxWidth and maxHeight can be 0...Albumenize
C
42

I believe that when the LRUCache evicts an image to make room for another one, the memory is not being freed.

It won't be, until the Bitmap is recycled or garbage-collected.

A quick google search reveals that this is happening because the image which is displaying has been recycled.

Which is why you should not be recycling there.

Why are recycled images still in the LRUCache if I'm only recycling them after they've been removed?

Presumably, they are not in the LRUCache. They are in an ImageView or something else that is still using the Bitmap.

What is the alternative for implementing a cache?

For the sake of argument, let's assume you are using the Bitmap objects in ImageView widgets, such as in rows of a ListView.

When you are done with a Bitmap (e.g., row in a ListView is recycled), you check to see if it is still in the cache. If it is, you leave it alone. If it is not, you recycle() it.

The cache is simply letting you know which Bitmap objects are worth holding onto. The cache has no way of knowing if the Bitmap is still being used somewhere.

BTW, if you are on API Level 11+, consider using inBitmap. OutOMemoryErrors are triggered when an allocation cannot be fulfilled. Last I checked, Android does not have a compacting garbage collector, so you can get an OutOfMemoryError due to fragmentation (want to allocate something bigger than the biggest single available block).

Chrischrism answered 25/5, 2012 at 0:20 Comment(14)
Thanks for the insightful comments. The key was "The cache is simply letting you know which Bitmap objects are worth holding onto. The cache has no way of knowing if the Bitmap is still being used somewhere." I was thinking about how LRUCache works all wrong. PS I love your books!Ulterior
@CommonsWare: How about situation when you check that recycled ListView's item and its bitmap is still in LRUCache? Eventually it'll be removed from LRUCache but since it isn't tied to any item, recycle() won't be called.Aalto
@Javanator: If the View passed in to your getView() method is not null, that is a row that ListView is asking you to recycle. Note that this is automatically handled for you in CursorAdapter and its subclasses.Chrischrism
@Chrischrism How to find which row in a ListView is recycled ?Tambratamburlaine
@CommonsWare. Thanks for your reply. As i have only one LRUCache map carrying my bitmaps and from here i assign these bitmap to my imageview in listview. I should have one more collection(map) carrying the same bitmap objects and then for recycled view checking bitmap exist in LRUCache. If not get the object from my other map and recycle and null it. Am i making sense. Will it work?Tambratamburlaine
@Javanator: "Am i making sense" -- not really, but I am short on sleep, so that may be my fault. :)Chrischrism
@Chrischrism ahhh. can understand. "you check to see if it is still in the cache" that means i have to maintain more then one reference of bitmap. one in lrucache and one locally ?Tambratamburlaine
@Javanator: A cache entry has a cache key (e.g., a URL) and a cache value (e.g., the bitmap). Given the URL, you see if the cache has an entry for it or not.Chrischrism
@Chrischrism "If it is not, you recycle()" If it is not present in the cache how can i get the reference of the bitmap object to call recyle on it. I have one LRUCache map and from there i get the bitmap and assign that to imagview directly without maintaining any other reference. The place where i am generating bitmap is adding things in the cache and forgets about it. cache has the only reference of bitmap generated to be used by viewTambratamburlaine
@Javanator: "If it is not present in the cache how can i get the reference of the bitmap object to call recyle on it" -- in the specific case cited here, you would need to get the BitmapDrawable from the ImageView, at the point in time of the row recycling.Chrischrism
@Chrischrism ((BitmapDrawable)image.getDrawable()).getBitmap(); Got it. Thats what i need to know. I will try it out. Will add a Separate Question if failed on anything. Many thanks for your time. I like your stuff too :)Tambratamburlaine
@Chrischrism i faced the same what Digger commented above and he was right. I posted the complete solution below taken from developer.android.com samples which is taking care of that too.Tambratamburlaine
@Javanator part of the getView or adapter would I check to see if bitmap should be recycled?Umbel
@Umbel check the setImageDrawable method of the RecyclingImageView i posted below. It is taking care of that whether view should be recycled or not. getView of adapter and onEntryRemoved of cache is changing the reference count of image accordingly. check the full solution below. Let me know if you need more information.Tambratamburlaine
T
18

Faced the same and thanks to @CommonsWare for the discussion. Posting the full solution here so it helps more people coming here for the same issue. Edits and Comments are welcomed. Cheers

 When should I recycle a bitmap using LRUCache?
  • Precisely when your Bitmap is neither in cache and nor getting referenced from any ImageView.

  • To maintain the reference count of bitmap we have to extend the BitmapDrawable class and add reference attributes to them.

  • This android sample has the answer to it exactly. DisplayingBitmaps.zip

We will get to the detail and code below.

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

Not exactly.

  • In entryRemoved delegate check whether Bitmap is still referenced from any ImageView. If not. Recycle it there itself.

  • And vice versa which is mentioned in the accepted answer that when view is about to get reused or getting dumped check its bitmap (previous bitmap if view is getting reused) is in the cache. If it is there leave it alone else recycle it.

  • The key here is we need to make check at both the places whether we can recycle bitmap or not.

I will explain my specific case where i am using LruCache to hold bitmaps for me. And displaying them in ListView. And calling recycle on bitmaps when there are no longer in use.

RecyclingBitmapDrawable.java and RecyclingImageView.java of the sample mentioned above are the core pieces we need here. They are handling things beautifully. Their setIsCached and setIsDisplayed methods are doing what we need.

Code can be found in the sample link mentioned above. But also posting the full code of file in the bottom of answer in case in future the link goes down or changed. Did a small modification of overriding setImageResource also to check state of previous bitmap.

--- Here goes the code for you ---

So your LruCache manager should look something like this.

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


And your getView() of ListView/GridView adapter should look normal like usual. As when you are setting a new image on ImageView using setImageDrawable method. Its internally checking the reference count on previous bitmap and will call recycle on it internally if not in 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;

    }

Here is your 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);
            }
        }
    }

}

Here is your 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();
    }

}
Tambratamburlaine answered 4/4, 2014 at 7:10 Comment(1)
Wow i spent hours trying to figure out how to fix my OOME. Thank you for a GREAT explanation!! THIS WORKSDialyse

© 2022 - 2024 — McMap. All rights reserved.