Android How to recycle bitmap correctly when using RecyclerView?
Asked Answered
C

1

11

As google's said we must manually call Bitmap.recycle() when the bitmap not use below Android 3.0 because memory are keep in native heap.

So we could have a reference count for a Bitmap and check if need to recycle bitmap in ImageView's onDetachedFromWindow() when using ListView. This is the solution from Google's demo project Bitmapfun(ImageFetcher).

But when using RecyclerView , convertview often be detached and attached. onDetachedFromWindow() maybe recycle the bitmap so when it attached to parent again the bitmap was recycled.

How to deal with this problem ? What's the correct way to recycle bitmap below Android 3.0 when using RecyclerView ?

This is Googls's Demo BitmapFun(ImageFetcher) 's solucation -- extend ImageView:

@Override
protected void onDetachedFromWindow() {
    // This has been detached from Window, so clear the drawable
    setImageDrawable(null);

    super.onDetachedFromWindow();
}

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

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

And also in LruCache when removed from memory cache:

            @Override
            protected void entryRemoved(boolean evicted, String key,
                    BitmapDrawable oldValue, BitmapDrawable newValue) {
                if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
                    // The removed entry is a recycling drawable, so notify it
                    // that it has been removed from the memory cache
                    ((RecyclingBitmapDrawable) oldValue).setIsCached(false);

The RecyclingBitmapDrawable is:

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()) {
        if (BuildConfig.DEBUG) {
            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();
}

}

The important point is in ImageView's onDetachedFromWindow : setImageDrawable(null) means clear drawable, it may make current drawble's ref count = 0 and recycle it! This solution work well when using ListView becouse onDetachedFromWindow only happen when ListView's root activity being destroyed.

But using Recyclering is different, this Reusing ConvertView's Mechanism is not same as ListView. ImageView maybe often detached and it is not the correct time to recycle Bitmap!

Choir answered 11/1, 2016 at 13:13 Comment(0)
I
0

You can use glide for that. Glide will load images efficiently and you can request it to clear the resources whenever needed.

You'll got to be using onViewRecycled() method of recyclerview adapter for that. You'll clear the resources using clear() method of glide like this,

override fun onViewRecycled(holder: VHCustom) {
        super.onViewRecycled(holder)
        Glide.with(ctx).clear(holder.imageView)
    }
Instrumentation answered 7/8, 2023 at 4:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.