什么时候应该使用LRUCache回收一个位图?

我正在使用LRUCache来caching存储在文件系统上的位图。 我基于这里的例子构build了caching: 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

快速谷歌search显示,这是因为正在显示的图像已被回收。

那么这里发生了什么? 为什么回收的图像仍然在LRUCache,如果我只是回收他们后,他们已经被删除? 什么是实施caching的替代scheme? Android文档明确指出,LRUCache是​​要走的路,但他们没有提到需要回收位图或如何这样做。

解决scheme万一它对其他人有用,解决这个问题的接受答案build议是不做我在上面的代码示例中做的(不要回收entryRemoved()调用中的位图)。

相反,当你完成一个ImageView(例如一个activity中的onPause() ,或者当一个视图被回收到一个适配器中时)检查位图是否仍然在caching中(我向caching中添加了一个isImageInCache()方法类),如果不是,则回收位图。 否则,不要pipe它。 这固定了我的OutOfMemoryexception,并阻止了仍在使用的回收位图。

我相信当LRUCache驱逐一个映像为另一个映像腾出空间时,内存不会被释放。

它不会,直到Bitmap被回收或垃圾收集。

快速谷歌search显示,这是因为正在显示的图像已被回收。

这就是为什么你不应该在那里回收。

为什么回收的图像仍然在LRUCache,如果我只是回收他们后,他们已经被删除?

据推测,他们不在LRUCache 。 他们在一个ImageView或其他东西仍然使用Bitmap

什么是实施caching的替代scheme?

为了说明起见,我们假设您正在使用ImageView小部件中的Bitmap对象,例如ListView行中。

当你完成一个Bitmap (例如,在一个ListView行被回收),你检查它是否仍然在caching中。 如果是这样,你不要pipe它。 如果不是,你recycle()它。

caching只是让你知道哪些Bitmap对象值得保存。 caching无法知道Bitmap是否仍在某处使用。

顺便说一句,如果你在API级别11+,考虑使用inBitmapOutOMemoryErrors在分配无法完成时触发。 最后我检查了一下,Android没有压缩的垃圾回收器,所以你可以通过碎片来获得一个OutOfMemoryError (想要分配比最大的可用块大的东西)。

面对同样的感谢@CommonsWare的讨论。 在这里发布完整的解决scheme,这有助于更多的人来到这里为同一个问题。 欢迎编辑和评论。 干杯

  When should I recycle a bitmap using LRUCache? 
  • 准确地说,当你的位图既不在caching中,也不从任何ImageView引用。

  • 为了保持位图的引用计数,我们必须扩展BitmapDrawable类并为它们添加引用属性。

  • 这个android样本准确的答案。 DisplayingBitmaps.zip

我们将会得到下面的细节和代码。

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

不完全是。

  • 在entryRemoved委托检查Bitmap是否仍然从任何ImageView引用。 如果不。 在那里回收它。

  • 反之亦然,在接受的答案中提到,当视图即将被重用或被转储时,检查其位图(如果视图正被重用,则以前的位图)在caching中。 如果是这样的话,别忘了还回收它。

  • 这里的关键是我们需要检查两个地方是否可以回收位图。

我将解释我的具体情况,我正在使用LruCache为我保存位图。 并在ListView中显示它们。 并在不再使用的位图上调用回收。

上述示例的RecyclingBitmapDrawable.javaRecyclingImageView.java是我们在这里需要的核心部分。 他们正在处理的事情美丽。 他们的setIsCachedsetIsDisplayed方法正在做我们需要的。

代码可以在上面提到的示例链接中find。 而且在将来链接发生故障或更改的情况下,还会在答案底部发布完整的文件代码。 重写setImageResource的一个小的修改也检查以前的位图的状态。

—这里是你的代码—

所以你的LruCachepipe理器应该看起来像这样。

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上设置新图像一样。 它在内部检查前一个位图的引用计数,如果不在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; } 

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