BitmapFactory OOM让我抓狂

31

我进行了大量搜索,发现很多人都遇到了使用BitmapFactory时出现的OOM内存问题。我的应用程序显示可用总内存为4MB,使用Runtime.getRuntime().totalMemory()测量。如果限制是16MB,为什么总内存不会增加以为位位图腾出空间呢?而是抛出错误。

我也不明白,如果根据Runtime.getRuntime().freeMemory(),我有1.6MB的可用内存,为什么会出现“VM不允许我们分配614400字节”的错误?看起来我有足够的可用内存。

我的应用程序已经完成,除了这个问题,但当我重启手机,只运行我的应用程序时,问题就消失了。我在使用HTC Hero进行设备测试(Android 1.5)。

目前,我认为唯一的解决方法是避免使用BitmapFactory

有没有人对此有什么想法或者能解释一下为什么VM在有1.6MB的可用内存时不会分配614KB?


这个可能会有帮助! - Rupesh Yadav
5个回答

47
请注意,本回答中的整个方法仅适用于2.3.x(姜饼)及以下版本(如CommonsWare在下面指出)。从Honeycomb开始,位图数据分配在VM堆中。
位图数据不是在VM堆中分配的。VM堆中有对它的引用(很小),但实际数据是由底层Skia图形库在Native堆中分配的。
不幸的是,虽然BitmapFactory.decode...()的定义说如果无法解码图像数据则返回null,但Skia实现(或者更确切地说是Java代码和Skia之间的JNI粘合剂)记录了您看到的消息(“VM won't let us allocate xxxx bytes”),然后抛出一个具有误导性信息“bitmap size exceeds VM budget”的OutOfMemory异常。
问题不在VM堆中,而是在Native堆中。Native堆在运行的应用程序之间共享,因此可用空间取决于其他应用程序正在运行以及它们的位图使用情况。但是,考虑到BitmapFactory不会返回,您需要一种在进行调用之前确定调用是否成功的方法。
有一些例程可以监视Native堆的大小(请参见Debug类getNative方法)。然而,我发现getNativeHeapFreeSize()和getNativeHeapSize()不可靠。因此,在我的一个动态创建大量位图的应用程序中,我执行以下操作。
Native堆大小因平台而异。因此,在启动时,我们检查最大允许的VM堆大小以确定最大允许的Native堆大小。[这些魔术数字是通过在2.1和2.2上进行测试确定的,可能在其他API级别上不同。]
long mMaxVmHeap     = Runtime.getRuntime().maxMemory()/1024;
long mMaxNativeHeap = 16*1024;
if (mMaxVmHeap == 16*1024)
     mMaxNativeHeap = 16*1024;
else if (mMaxVmHeap == 24*1024)
     mMaxNativeHeap = 24*1024;
else
    Log.w(TAG, "Unrecognized VM heap size = " + mMaxVmHeap);

每次我们需要调用BitmapFactory时,都需要在调用之前进行以下形式的检查。

long sizeReqd        = bitmapWidth * bitmapHeight * targetBpp  / 8;
long allocNativeHeap = Debug.getNativeHeapAllocatedSize();
if ((sizeReqd + allocNativeHeap + heapPad) >= mMaxNativeHeap)
{
    // Do not call BitmapFactory…
}

请注意,heapPad是一个神奇的数字,它允许考虑到以下两个因素:a) Native heap大小的报告是“软性”的;b) 我们希望在Native heap中为其他应用程序留出一些空间。目前我们使用3*1024*1024(即3MB)的pad。

一种非常有趣和实用的方法。 - BonanzaDriver
这是基于阅读Skia源代码,特别是查看它如何处理bitmap.recycle()的。但回想起来,虽然我在那段代码中没有看到任何针对每个应用程序的引用/处理(这就是导致我做出那个声明的原因),但现在我认为那太强烈/错误了。本地堆将按应用程序分配,依赖于VM的正常进程内存处理,因此Skia代码不必担心每个应用程序/进程问题。 - Torid
你可以使用 http://developer.android.com/reference/android/graphics/BitmapFactory.Options.html 中的 inJustDecodeBounds 选项,在解码之前获取边界。 - Torid
1
获取bpp的一种方法是使用getWindow().getAttributes().format来获取PixelFormat(http://developer.android.com/reference/android/graphics/PixelFormat.html)。从那里,您可以映射到bpp(例如,OPAQUE为16pp)。 - Torid
5
自 Honeycomb 版本起,位图数据被分配在虚拟机堆上,而不是未分配在虚拟机堆上。原文:"Bitmap data is not allocated in the VM heap" -- it is allocated on the VM heap as of Honeycomb. - CommonsWare
显示剩余5条评论

2

虽然1.6MB的内存听起来很多,但也有可能是内存碎片化太严重,无法一次性分配这么大的内存块(不过这听起来确实很奇怪)。

在使用图像资源时,OOM的一个常见原因是解压具有高分辨率的JPG、PNG、GIF图像。需要注意的是,所有这些格式都经过了相当好的压缩,占用非常少的空间,但一旦将图像加载到手机上,它们将使用的内存大约为宽度*高度*4字节。此外,在解压缩开始时,还需要加载几个其他辅助数据结构以进行解码步骤。


2
似乎在更近的Android版本中,Torid的答案中提到的问题已经得到解决。
然而,如果您正在使用图像缓存(专用缓存或普通HashMap),通过创建内存泄漏很容易出现此错误。
根据我的经验,如果您无意中保留了Bitmap引用并创建了内存泄漏,则OP的错误(指BitmapFactory和本地方法)将导致应用程序崩溃(最高到ICS-14及以上)。
为避免这种情况,请确保“释放”您的位图。这意味着在缓存的最后一层中使用SoftReferences,以便位图可以从其中进行垃圾回收。这应该有效,但如果仍然出现崩溃,您可以尝试使用bitmap.recycle()明确标记某些位图进行收集,但请记住,如果bitmap.isRecycled(),请勿返回位图供应用程序使用。
另外,LinkedHashMaps是实现相当不错的缓存结构的好工具,特别是如果您像这个例子(从第308行开始)中所示的那样组合硬引用和软引用...但是,如果出现错误,使用硬引用也会导致内存泄漏。

0

通常情况下,捕获错误并没有意义,因为通常它们只会被虚拟机抛出,但在这种特殊情况下,错误是由jni粘合代码抛出的,因此处理无法加载图像的情况非常简单:只需捕获OutOfMemoryError即可。


0

虽然这是一个相当高层次的答案,但对我来说问题在于在所有视图上使用硬件加速。我的大多数视图都有自定义位图操作,我认为这是大型本地堆大小的来源,但实际上当禁用硬件加速时,本地堆使用量减少了4倍。

似乎硬件加速会在您的视图上进行各种缓存,创建自己的位图,并且由于所有位图共享本地堆,分配大小可能会显著增长。


1
@Henrique Sousa:在AndroidManifest.xml文件中的你的活动(activity)中添加android:hardwareAccelerated="false"。 - samgak
令人惊讶的是,这解决了问题...我简直不敢相信谷歌对这个四倍内存使用量的文档记录如此之差! - Henrique de Sousa

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