启用largeHeap后的位图回收

10
在启用 largeHeap 选项之前,我正在处理大位图,并且几乎消耗了应用程序可用的所有内存,通过在导航和加载新的位图时进行回收,它可以在几乎完整的堆上运行。但是当某些操作需要更多内存时,应用程序会崩溃。因此,我启用了 largeHeap=true 来获得更多内存。
但是这样做会产生意外的行为,似乎位图的 recycle() 方法大多数情况下都不起作用,原来在58MB内存中运行的应用程序(并有时超出抛出 OutOfMemoryException)现在以指数方式消耗内存并继续增长(目前测试所显示分配了231MB内存),期望的行为是内存管理仍然正常工作,应用程序不会使用超过60MB。
如何避免这种情况?或有效地回收位图?
编辑:实际上,在设备上分配超过390MB的内存时,我使它产生 OutOfMemoryError。 阅读 GC_* 日志表明,只有 GC_FOR_ALLOC 有时释放了3.8MB,但几乎从未释放其他 GC 运行的内容。

1
你看过这个很棒的视频吗?http://www.youtube.com/watch?v=_CruQY55HOk 我猜你的代码中存在内存泄漏。 - HitOdessit
你需要告诉我们这是在Honeycomb之前还是大于等于Honeycomb版本。 - Joe Plante
5个回答

21

你可能应该查看高效显示位图,其中包括多种处理大位图的方法。

  • 高效加载大位图
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
这将为您提供下载前的图像大小,基于此,您可以使用在文档解释中提供的 calculateInSampleSize()decodeSampledBitmapFromResource() 方法来检查设备的大小并进行比例缩放。计算需要缩放图像的比例,可参考以下方式:第一种方法
if (imageHeight > reqHeight || imageWidth > reqWidth) {
      if (imageWidth > imageHeight ) {
          inSampleSize = Math.round((float)imageHeight / (float)reqHeight);
      } else {
          inSampleSize = Math.round((float)imageWidth / (float)reqWidth);
      }
    }
int inSampleSize = Math.min(imageWidth / reqWidth,imageHeight / reqHeight);
你可以设置inSampleSize
 options.inSampleSize = inSampleSize;

最后确保你调用该函数:

options.inJustDecodeBounds = false;

否则它将返回null作为位图。

  • 在UI线程之外处理位图

    在UI线程上处理位图永远不安全,因此最好在后台线程中进行处理,并在过程完成后更新UI。

  • 缓存位图

    LruCache 自API 12起可用,但如果您对以下版本感兴趣,则也可以在Support Library中使用它。因此,应该使用它有效地缓存图像。还可以使用DiskLruCache来存储那些需要长时间留存在外部存储器中的图像。

  • 清除缓存

    有时,当您的图像大小太大时,即使缓存图像也会导致OutOfMemoryError,因此在这种情况下,最好在图像超出范围或长时间未使用时清除缓存,以便其他图像可以被缓存。

    我曾经为此创建了一个演示示例,您可以从这里下载。


2
管理位图的答案不错,但如何“高效地回收位图”呢? - ALiGOTec
为什么要回收位图?如果位图被回收,它就不能被重复使用,这将导致图像的异常下载。因此,最好使用位图缓存。 - Lalit Poptani
1
问题实际上是关于高效地回收位图,因为我已经按照管理位图文章的指导进行了实现。即使使用缓存对我也没有用,因为显示的图像在请求之前无法被收集。 - Marcos Vasconcelos
我必须说,我现在使用ljnigraphics库进行本地绘图(以前我移动一些巨大的ByteBuffers来绘制图像),所以我必须关闭largeHeap功能,但问题仍然存在。使用largeHeap时,垃圾回收器根本不会回收任何东西。 - Marcos Vasconcelos
好的,这个答案只是来自于Android开发文章中“高效显示位图”的简述。我仍然会授予您奖励,因为我不会自私地回答并授予自己。 - Marcos Vasconcelos
这是一个很好的解释 +1。 - neferpitou

3

您的情况表现如预期。 在蜂窝版本之前,recycle() 无条件释放内存。 但在3.0及以上版本中,位图是普通垃圾收集内存的一部分。 您的设备有足够的RAM,允许JVM分配超过58M的内存限制,现在垃圾收集器满意并且没有动力回收由您的位图占用的内存。

您可以通过在控制内存量的模拟器上运行,或在设备上加载一些消耗内存的服务来验证此操作 - 垃圾收集器将开始工作。 您可以使用DDMS进一步研究内存使用情况。

您可以尝试一些解决位图内存管理的方案:Android中的位图 位图内存泄漏 http://blog.javia.org/how-to-work-around-androids-24-mb-memory-limit/,但首先请从官方Android位图提示 开始,正如 @Lalit Poptani 在他的详细回答中所解释的那样。

请注意,将位图移动到OpenGL内存中作为纹理会对性能产生一些影响(但是如果最终通过OpenGL渲染这些位图则非常完美)。 纹理和malloc解决方案都需要您显式释放不再使用的位图内存。


2

肯定的,@Lalit Poptani 的回答是解决这个问题的方法,如果你的 Bitmaps 非常大,你应该确实要把它们缩小。如果可能的话,最好的方法是在 server-side 上完成此操作,因为这样还可以减少 NetworkOperation 的时间。

关于实现 MemoryCacheDiskCache,这仍然是最佳的方法,但我仍然建议使用一个现有的库,它正是这样做的(Ignition),这样你就可以节省很多时间,而且也可以避免很多内存泄漏,因为你的 HeapGC 后没有被清空,我可以想象你可能也有一些 memory leaks


我真的会“搜索和销毁”内存泄漏,内存在导航时按预期回收。 - Marcos Vasconcelos

1
为了解决您的困境,我认为这是预期行为。
如果您想释放内存,可以偶尔调用System.gc(),但实际上大部分时间应该让它自己管理垃圾回收。
我建议您保留一个简单的缓存(url/文件名到位图),通过计算每个位图占用的字节数来跟踪其自身的内存使用情况。
/**
 * Estimates size of Bitmap in bytes depending on dimensions and Bitmap.Config
 * @param width
 * @param height
 * @param config
 * @return
 */
public static long estimateBitmapBytes(int width, int height, Bitmap.Config config){
    long pixels=width*height;
    switch(config){
    case ALPHA_8: // 1 byte per pixel
        return pixels;
    case ARGB_4444: // 2 bytes per pixel, but depreciated
        return pixels*2;
    case ARGB_8888: // 4 bytes per pixel
        return pixels*4;
    case RGB_565: // 2 bytes per pixel
        return pixels*2;
    default:
        return pixels;
    }
}

然后您查询应用程序正在使用多少内存以及可用多少内存,可能会取其中一半并尝试将总图像缓存大小保持在该范围内,通过在达到此限制时从列表中简单地删除(取消引用)旧图像,而不是回收。让垃圾收集器在它们从缓存中取消引用且不被任何视图使用时清理位图。

/**
 * Calculates and adjusts the cache size based on amount of memory available and average file size
 * @return
 */
synchronized private int calculateCacheSize(){
    if(this.cachedBitmaps.size()>0){
        long maxMemory = this.getMaxMemory(); // Total max VM memory minus runtime memory 
        long maxAllocation = (long) (ImageCache.MEMORY_FRACTION*maxMemory);
        long avgSize = this.bitmapCacheAllocated / this.cachedBitmaps.size();
        this.bitmapCacheSize = (int) (maxAllocation/avgSize);
    }
    return this.bitmapCacheSize;
}

我建议你远离使用 recycle(),它会导致许多间歇性异常(例如当看起来已经完成的视图尝试访问已回收的位图时),并且总体上看起来很有缺陷。

好的,我已经延迟了对象,但在回收之前,我会尝试一下不调用recycle()。 - Marcos Vasconcelos

1
在Android上处理位图时必须非常小心。让我来重新表述一下:即使在具有4GB RAM的系统上,处理位图也要小心谨慎。这些文件有多大?你可能需要将其分割成块。请记住,您使用的是视频RAM,这与系统RAM不同。
在Honeycomb之前,位图是在C++层上分配的,因此RAM使用量对Java不可见,并且无法被垃圾回收器访问。一个RGB24色彩空间的3 MP未压缩位图使用约9-10兆字节(大约2048x1512)。因此,较大的图像可以轻松填满堆。还要记住,在用于视频RAM的任何内容中(有时是专用RAM,有时与系统共享),数据通常以未压缩形式存储。
基本上,如果您的目标是Honeycomb之前的设备,则几乎必须像编写C++程序一样管理位图对象。如果屏幕上有很多图像,则在onDestory()上运行位图recycle()通常有效,但如果屏幕上有大量图像,则可能需要实时处理它们。此外,如果启动另一个活动,则可能需要考虑在onPause()和onResume()中加入逻辑。

当图片不在视频RAM中时,您也可以使用Android文件系统或SQLite缓存图片。如果您使用的是像.jpg或具有大量重复数据/的.png格式,则可以尝试将其缓存在RAM中。


1
这是一个情况,应用程序本身分页显示了大量的图像。我使用循环利用来实时处理它们,效果非常好。 - Marcos Vasconcelos

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