我的图像缓存处理过程会泄漏内存吗?

4
我有一个对象,覆盖了Application对象。在里面,我有一个成员变量,它是一个LongSparseArray,其中键是类型为long的某个标识符,而值是具有两个成员变量的对象:一个Bitmap和一个用作时间戳的long
这是我的全局图像缓存。偶尔会运行一个函数,查看时间戳并使超过一小时的条目过时。
通过“过时”,我指的是从LongSparseArray中删除整个条目。
我的问题是:
假设我有一个带有ListViewActivity。每个ListView中的行都有一个ImageView,它使用缓存中的图像填充。
Bitmap image = ((MyApp)getApplicationContext()).getImage(id);
holder.imgImage.setImageBitmap(image);

现在,假设用户点击某个按钮,该按钮会将他们带到一个新的Activity。当在这个新的Activity上时,之前分配给在上一个ActivityListView中一行的图像已经过时。
因此,简要概括一下,在全局LongSparseArray中,该Bitmap键/值条目现在不再存在。
那么,这个Bitmap能被Java回收吗?它还被ListView中的ImageView引用,前面的Activity中使用了它吗?当然,假设Android没有回收该Activity所使用的内存。
我问这个问题的原因是,我的上一个老化函数也会在Bitmap上调用.Recycle()。在这种情况下,当用户点击返回按钮并返回到先前使用该BitmapActivity时,应用程序会崩溃,可能是因为该Bitmap不仅从缓存中消失,而且还从内存中消失。所以我只是删除了.Recycle()调用。
顺便说一下,一旦Bitmap从缓存中删除,并且具有该ID的对象再次显示在屏幕上,应用程序将重新下载Bitmap并将其放入缓存中。如果以前的内存仍然存在,您可以看到这会产生问题。
此外,有没有人有更有效的解决方案?
如果我设置myImageView.setDrawingCacheEnabled(false);会发生什么?
有两个Activities使用此图像缓存。一个是搜索屏幕,在用户执行搜索后显示项目列表(及其图像)。另一个是所选项目列表。

1
O' CommonsWare,O' CommonsWare,你在哪里,CommonsWare? - Andrew
3个回答

4
问题:一旦在位图上调用recycle()方法,该位图就不应再次使用。如果尝试绘制该位图,则会抛出异常。来自docs
“仅当您确定不再使用位图时才应使用recycle()。如果调用recycle()然后稍后尝试绘制该位图,则会收到错误:“Canvas:trying to use a recycled bitmap”。”
在这种特定情况下,您已回收了位图,但ListView项的ImageView对该位图具有强引用。当返回Activity时,ListView项尝试绘制该位图,因此引发异常。
位图存储管理:在Android 2.3.3之前,位图的后备像素数据存储在本机内存中,而位图本身存储在Dalvik内存中。因此,必须调用recycle方法以释放本机内存。

这里是Bitmap.recycle函数的定义:

    public void recycle() {
        if (!mRecycled) {
            if (nativeRecycle(mNativeBitmap)) {
                // return value indicates whether native pixel object was actually recycled.
                // false indicates that it is still in use at the native level and these
                // objects should not be collected now. They will be collected later when the
                // Bitmap itself is collected.
                mBuffer = null;
                mNinePatchChunk = null;
            }
            mRecycled = true;
        }
    }

Android 3.0之后,支持像素数据存储在Dalvik内存中。当不再需要位图时,我们需要确保不保留对位图的任何强引用,以便进行垃圾回收。

解决方案:如果您仍在支持Android 2.3.3及更低版本,则仍需要使用recycle释放位图。

您可以使用引用计数来跟踪ListView项目当前是否正在引用位图,以便即使它变老,也不会在位图上调用recycle

ListView适配器的getView方法是将位图分配给ImageView的位置。在这里,您会增加引用计数。您可以将setRecyclerListener附加到ListView上,以知道何时将列表项放入回收站。这是您将减少位图引用计数的位置。只有当引用计数为零时,老化函数才需要回收位图。

你还可以考虑使用LruCache进行缓存,如文档中所述。 setDrawingCacheEnabled: 通过将此方法调用为true参数,下一次调用getDrawingCache将绘制视图到位图。视图的位图版本可以呈现在屏幕上。由于它只是一个位图,我们不能像实际视图那样与之交互。几个用例包括:
  • ListView被滚动时,捕获并呈现显示项视图的位图,以便正在滚动的视图不需要进行测量和布局传递。
  • DDMS中的视图层次结构功能。

我正在考虑切换到LruCache,但我不明白它如何避免上述过程。您可以在此处查看我的疑虑(http://stackoverflow.com/questions/25628734/implementing-a-global-lrucache-vs-many-local-lrucaches)。此外,我目前仍在支持2.3.3版本,尽管这可能很快会改变。 - Andrew
@Andrew 我已经在这里回答了你的疑虑(http://stackoverflow.com/a/25634577/2083078)。 - Manish Mulimani
感谢您的回复,Manish。 - Andrew

0

Bitmap真的能够被Java回收吗?毕竟它仍然被前一个Activity中ListView中ImageView所引用,假设Android没有回收那个Activity所使用的内存。

Bitmap仍在ListView中使用(强引用),因此Dalvik无法回收其内存。

显然,您不能调用recycle方法以回收Bitmap对象,否则会出现错误(例如应用程序崩溃)。

如果我将myImageView.setDrawingCacheEnabled(false)设置为false会发生什么?

如果禁用绘图缓存,则每次需要重绘视图时,都将调用onDraw方法。我不是非常熟悉ImageView,您可以查看其源代码进行深入了解。 (注意:当启用/禁用硬件加速时,绘图缓存的使用方式不同,在这里,我只假设您使用软件渲染)。

解决方案如下:

  1. 当位图缓存变得陈旧时,您需要将其从缓存数组中移除(然后您的应用程序将尝试获取新的位图,我认为是这样)。
  2. ListView.getView 中,您可以检查当前使用的位图的年龄。这很容易做到,因为您知道第一次调用 setImageBitmap 和最新时间戳。如果它们不同,则使用新的位图再次调用 setImageBitmap,旧的位图将被回收。

希望这能帮到您。


每次调用getView()时,它都会命中图像缓存。问题是,如果一个位于Activity堆栈中间的Activity使用了已经从缓存中弹出的旧图像之一,那怎么办?在onPause()或其他什么地方,有没有办法告诉所有ActivityImageView停止指向该位图? - Andrew
这似乎是许多应用程序都会遇到的问题。那么它们是如何解决的呢?为什么我以前从未见过这个问题被讨论过? - Andrew

0

关于“还有没有更有效的解决方案?”

Picasso库可以帮助解决您面临的问题 http://square.github.io/picasso/

Picasso是“Android平台上功能强大的图像下载和缓存库”

“Picasso自动处理了Android上图像加载的许多常见陷阱:

  • 在适配器中处理ImageView回收和下载取消。
  • 自动内存和磁盘缓存。”

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