何时应该使用LRUCache回收位图?

56
我正在使用LRUCache缓存存储在文件系统上的位图。我根据这里的示例构建了缓存:http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html 问题是在使用应用程序时经常出现OutOfMemory崩溃。我认为当LRUCache清除一张图片以为另一张图片腾出空间时,内存没有被释放。
当图像被驱逐时,我添加了对Bitmap.recycle()的调用:
  // 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;
                }
            };

这个修复了应用程序的崩溃问题,但有时候会导致图片在应用程序中不显示(只呈现出黑色空白)。每次发生这种情况,我在Logcat中都会看到以下信息:Cannot generate texture from bitmap

快速谷歌搜索表明,这是由于正在显示的图像已被回收造成的。

那么发生了什么?如果我只是在图像被删除后才将它们回收,为什么回收的图像仍然存在于LRUCache中?实现缓存的替代方法是什么? Android文档明确指出LRUCache是正确的选择,但它们并没有提到需要回收位图或如何回收位图。

解决方案: 如果对其他人有用的话,建议采纳下面答案中提出的解决方案,不要像我的代码示例一样在entryRemoved()调用中回收位图。

相反,在使用ImageView结束时(例如在活动的onPause()中,或者在适配器中回收视图时),检查位图是否仍然在缓存中(我在缓存类中添加了一个isImageInCache()方法),如果不在,则回收该位图。否则,将其保留。这解决了我的OutOfMemory异常问题并防止回收仍在使用的位图。


10
你是如何检查位图仍然在缓存中的? - LuxuryMode
您可以使用ImageLoader来检查图像是否已缓存,例如imageLoader.isCached(url, maxWidth, maxHeight);maxWidth和maxHeight可以为0... - AjOnFire
2个回答

42
我认为当LRUCache为了给其他图片腾出空间而驱逐一张图片时,内存并没有被释放。直到Bitmap被回收或垃圾回收才会释放内存。一个快速的谷歌搜索显示,这是因为正在显示的图像已被回收。这就是为什么不应该在那里回收。如果我只在它们被移除后回收它们,为什么回收的图像仍然在LRUCache中?可能它们不在LRUCache中,而是在一个ImageView或其他仍在使用Bitmap的东西中。假设您正在将Bitmap对象用于ImageView小部件(例如ListView的行)等情况下,实现缓存的替代方案是什么?

当你完成一个Bitmap的使用(例如在ListView中回收一行),你需要检查它是否仍然在缓存中。如果是,就不要管它。如果不是,你需要使用recycle()方法进行回收。

缓存只是让你知道哪些Bitmap对象值得保留。但缓存无法知道这个Bitmap对象是否仍然在其他地方被使用。

顺便提一下,如果你的API等级在11或以上,可以考虑使用inBitmap。当内存分配无法满足需求时,会触发OutOMemoryErrors错误。据我所知,Android没有压缩垃圾收集器,因此由于碎片化(想要分配比可用块还大的东西)可能会导致OutOfMemoryError


2
感谢您的深入评论。关键在于“缓存只是让您知道哪些位图对象值得保留。缓存无法知道位图是否仍然在其他地方使用。”我一直对LRUCache的工作方式有误解。顺便说一句,我喜欢您的书! - howettl
8
当您检查回收的ListView项及其位图仍在LRUCache中时,情况怎样? 最终它将从LRUCache中删除,但由于它没有绑定到任何项,因此不会调用recycle()方法。 - Digger
@Javanator:如果传递给你的getView()方法的View不是null,那就是ListView要求你回收利用的一行。请注意,在CursorAdapter及其子类中,这个过程已经自动处理了。 - CommonsWare
1
@CommonsWare 如何找到 ListView 中被回收的行? - Rohit Sharma
1
@Javanator:“如果缓存中不存在,我该如何获取位图对象的引用以调用recycle?”--在这里引用的特定情况下,您需要在行回收时从ImageView中获取BitmapDrawable。 - CommonsWare
显示剩余9条评论

18
面对同样的问题,感谢@CommonsWare的讨论。在此发布完整的解决方案,以帮助更多遇到相同问题的人。欢迎进行编辑和评论。祝好!
 When should I recycle a bitmap using LRUCache?
  • 当你的位图既没有被缓存,也没有被任何ImageView引用时。

  • 为了维护位图的引用计数,我们需要扩展BitmapDrawable类并添加引用属性。

  • 这个Android示例程序正好回答了这个问题。 DisplayingBitmaps.zip

下面将详细介绍代码。

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

并不完全正确。
  • 在entryRemoved委托中检查Bitmap是否仍然从任何ImageView引用。 如果没有,请在那里回收它。

  • 反之亦然,即当视图即将被重用或被删除时,检查其位图(如果视图正在被重用,则为先前的位图)是否在缓存中。如果在那里就让它保持不变,否则将其回收。

  • 关键在于我们需要在两个地方都进行检查,无论是否可以回收位图。

我将解释我的具体情况,我在使用LruCache来保存位图。 并在ListView中显示它们。当不再使用时调用位图的recycle方法。

上面提到的样例中的RecyclingBitmapDrawable.javaRecyclingImageView.java是我们需要的核心组件。他们处理得非常好。 他们的setIsCachedsetIsDisplayed方法正是我们所需要的。

代码可以在上面提到的样例链接中找到。但是,为了防止未来链接失效或更改,本答案底部也发布了文件的完整代码。还对setImageResource进行了小的修改以检查先前位图的状态。

--- 这是你需要的代码 ---

因此,你的LruCache管理器应该类似于这样。

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


而您的ListView/GridView适配器中的getView()应该像往常一样正常显示。 就像您使用setImageDrawable方法在ImageView上设置新图像时一样。 它会在内部检查先前位图的引用计数,并在不在LRU缓存中时内部调用recycle。

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

    }

这里是您的 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);
            }
        }
    }

}

这是您的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();
    }

}

哇,我花了好几个小时来解决我的 OOME 问题。非常感谢你提供的出色解释!这个方法真的有效。 - galhe2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接