Android 我应该什么时候使用 LRUCache 回收位图?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/10743381/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-20 04:49:08  来源:igfitidea点击:

When should I recycle a bitmap using LRUCache?

androidbitmapandroid-lru-cache

提问by howettl

I'm using an LRUCacheto 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

我正在使用LRUCache来缓存存储在文件系统上的位图。我根据这里的示例构建了缓存: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.

问题是我在使用该应用程序时经常看到 OutOfMemory 崩溃。我相信当 LRUCache 驱逐一个图像为另一个图像腾出空间时,内存不会被释放。

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

当图像被驱逐时,我添加了对 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;
                }
            };

This fixes the crashes, however it also results in images sometimesnot 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.

这修复了崩溃,但它也会导致图像有时不会出现在应用程序中(只是图像应该出现的黑色空间)。无论何时发生,我都会在我的 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.

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

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

已解决:如果它对其他人有用,那么已接受的答案所建议的解决此问题的方法是不要执行我在上面的代码示例中所做的操作(不要在entryRemoved()调用中回收位图)。

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 OutOfMemoryexceptions and prevented recycling bitmaps which were still being used.

相反,当您完成 ImageView(例如onPause()在活动中,或在适配器中回收视图时)检查位图是否仍在缓存中(我isImageInCache()在缓存类中添加了一个方法),如果是不,则回收位图。否则,别管它。这修复了我的OutOfMemory异常并防止回收仍在使用的位图。

采纳答案by CommonsWare

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

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

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

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

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?

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

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

据推测,它们不在LRUCache. 他们在一个ImageView或其他仍在使用Bitmap.

What is the alternative for implementing a cache?

实现缓存的替代方法是什么?

For the sake of argument, let's assume you are using the Bitmapobjects in ImageViewwidgets, such as in rows of a ListView.

为了便于论证,我们假设您BitmapImageView小部件中使用对象,例如在 a 的行中ListView

When you are done with a Bitmap(e.g., row in a ListViewis 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.

当你用完 a 时Bitmap(例如,a 中的行ListView被回收),你检查它是否仍在缓存中。如果是,你就别管它了。如果不是,你recycle()就是。

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

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

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

顺便说一句,如果您在 API 级别 11+ 上,请考虑使用inBitmap. OutOMemoryErrors当分配无法完成时触发。最后我检查过,Android 没有压缩垃圾收集器,因此您可能会OutOfMemoryError因碎片而得到一个(想要分配比最大的单个可用块更大的东西)。

回答by Javanator

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

面临同样的问题,感谢@CommonsWare 的讨论。在这里发布完整的解决方案,这样可以帮助更多的人来这里解决同样的问题。欢迎编辑和评论。干杯

 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

  • 正是当您的位图既不在缓存中也不在任何 ImageView 中被引用时。

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

  • 这个 android 示例完全有答案。 显示位图.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.

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

  • 反之亦然,在接受的答案中提到,当视图即将被重用或被转储时,检查它的位图(如果视图被重用,则为前一个位图)是否在缓存中。如果它在那里,别管它,否则回收它。

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

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.

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

RecyclingBitmapDrawable.javaand RecyclingImageView.javaof the sample mentioned above are the core pieces we need here. They are handling things beautifully. Their setIsCachedand setIsDisplayedmethods are doing what we need.

上面提到的示例的RecyclingBitmapDrawable.javaRecyclingImageView.java是我们这里需要的核心部分。他们处理事情很漂亮。他们的setIsCachedsetIsDisplayed方法正在做我们需要的。

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.

代码可以在上面提到的示例链接中找到。但也要在答案底部发布文件的完整代码,以防将来链接断开或更改。对覆盖 setImageResource 也做了一个小的修改,以检查前一个位图的状态。

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

--- 这是给你的代码---

So your LruCache manager should look something like this.

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

LruCacheManager.java

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.


和你的getView()的ListView / GridView控件适配器应该看起来像正常如常。当您使用 setImageDrawable 方法在 ImageView 上设置新图像时。它在内部检查先前位图上的引用计数,如果不在 lrucache 中,则会在内部调用 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;

    }

Here is your RecyclingImageView.java

这是你的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

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

}