Android Bitmap 内存限制

在编写 Android 程序的时候,我们总是难免会碰到 OOM 的错误,那么这个错误究竟是怎么来的呢?我们先来看一下这段异常信息:

08-14 05:15:04.764: ERROR/dalvikvm-heap(264): 3528000-byte external allocation too large for this process.
08-14 05:15:04.764: ERROR/(264): VM won’t let us allocate 3528000 bytes
08-14 05:15:04.764: DEBUG/skia(264): — decoder->decode returned false
08-14 05:15:04.774: DEBUG/AndroidRuntime(264): Shutting down VM
08-14 05:15:04.774: WARN/dalvikvm(264): threadid=3: thread exiting with uncaught exception (group=0x4001b188)
08-14 05:15:04.774: ERROR/AndroidRuntime(264): Uncaught handler: thread main exiting due to uncaught exception
08-14 05:15:04.794: ERROR/AndroidRuntime(264): java.lang.OutOfMemoryError: bitmap size exceeds VM budget
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:447)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:323)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at android.graphics.BitmapFactory.decodeResource(BitmapFactory.java:346)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at android.graphics.BitmapFactory.decodeResource(BitmapFactory.java:372)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at com.xixun.test.HelloListView.onCreate(HelloListView.java:33)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2459)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2512)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at android.app.ActivityThread.access$2200(ActivityThread.java:119)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1863)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at android.os.Handler.dispatchMessage(Handler.java:99)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at android.os.Looper.loop(Looper.java:123)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at android.app.ActivityThread.main(ActivityThread.java:4363)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at java.lang.reflect.Method.invokeNative(Native Method)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at java.lang.reflect.Method.invoke(Method.java:521)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:860)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:618)
08-14 05:15:04.794: ERROR/AndroidRuntime(264):     at dalvik.system.NativeStart.main(Native Method)

从上面这段异常信息中,我们看到了一个 OOM(OutOfMemory) 错误,我称其为 (OMG 错误)。出现这个错误的原因是什么呢?为什么解码图像会出现这样的问题呢?关于这个问题,我纠结了一段时间,在网上查询了很多资料,甚至查看了 Android Issues,确实看到了相关的问题例如 Issue 3405Issue 8488,尤其 Issue 8488 下面一楼的回复,让我觉得很雷人啊:

Comment 1 by romain…@android.com, May 23, 2010

当然我们承认不好的程序总是程序员自己错误的写法导致的 ,不过我们倒是非常想知道如何来规避这个问题,那么接下来就是解答这个问题的关键。

我们从上面的异常堆栈信息中,可以看出是在 BitmapFactory.nativeDecodeAsset(),对应该方法的 native 方法是在 BitmapFactory.cpp 中的 doDecode() 方法,在该方法中申请 JavaPixelAllocator 对象时,会调用到 Graphics.cpp 中的 setJavaPixelRef() 方法,在 setJavaPixelRef() 中会对解码需要申请的内存空间进行一个判断,代码如下:

bool r = env->CallBooleanMethod(gVMRuntime_singleton,

                                   gVMRuntime_trackExternalAllocationMethodID,

                                   jsize);

而 JNI 方法 ID — gVMRuntime_trackExternalAllocationMethodID 对应的方法实际上是 dalvik_system_VMRuntime.c 中的 Dalvik_dalvik_system_VMRuntime_trackExternalAllocation(),而在该方法中又会调用大 HeapSource.c 中的 dvmTrackExternalAllocation() 方法,继而调用到 externalAllocPossible() 方法,在该方法中这句代码是最关键的

heap = hs2heap(hs);

   currentHeapSize = mspace_max_allowed_footprint(heap->msp);

   if (currentHeapSize + hs->externalBytesAllocated + n <=

           heap->absoluteMaxSize)

   {

       return true;

   }

这段代码的意思应该就是当前堆已使用的大小 (由 currentHeapSize 和 hs->externalBytesAllocated 构成) 加上我们需要再次分配的内存大小不能超过堆的最大内存值。那么一个堆的最大内存值究竟是多大呢。通过下面这张图,我们也许可以看到一些线索 (自己画的,比较粗糙)

image

最终的决定权其实是在 Init.c 中,因为 Android 在启动系统的时候会去优先执行这个里面的函数,通过调用 dvmStartup() 方法来初始化虚拟机,最终调用到会调用到 HeapSource.c 中的 dvmHeapSourceStartup() 方法,而在 Init.c 中有这么两句代码:

gDvm.heapSizeStart = 2 * 1024 * 1024;   // Spec says 16MB; too big for us.

gDvm.heapSizeMax = 16 * 1024 * 1024;    // Spec says 75% physical mem

在另外一个地方也有类似的代码,那就是 AndroidRuntime.cpp 中的 startVM() 方法中:

strcpy(heapsizeOptsBuf, "-Xmx");

property_get("dalvik.vm.heapsize", heapsizeOptsBuf+4, "16m");

//LOGI("Heap size: %s", heapsizeOptsBuf);

opt.optionString = heapsizeOptsBuf;

同样也是默认值为 16M,虽然目前我看到了两个可以启动 VM 的方法,具体 Android 何时会调用这两个初始化 VM 的方法,还不是很清楚。不过可以肯定的一点就是,如果启动 DVM 时未指定参数,那么其初始化堆最大大小应该就是 16M,那么我们在网上查到了诸多关于解码图像超过 8M 就会出错的论断是如何得出来的呢?

我们来看看 HeapSource.c 中的这个方法的注释

/*

* External allocation tracking

*

* In some situations, memory outside of the heap is tied to the

* lifetime of objects in the heap.  Since that memory is kept alive

* by heap objects, it should provide memory pressure that can influence

* GCs.

*/

static bool

externalAllocPossible(const HeapSource *hs, size_t n)

{

    const Heap *heap;

    size_t currentHeapSize;

   /* Make sure that this allocation is even possible.

     * Don’t let the external size plus the actual heap size

     * go over the absolute max.  This essentially treats

     * external allocations as part of the active heap.

     *

     * Note that this will fail "mysteriously" if there’s

     * a small softLimit but a large heap footprint.

     */

    heap = hs2heap(hs);

    currentHeapSize = mspace_max_allowed_footprint(heap->msp);

    if (currentHeapSize + hs->externalBytesAllocated + n <=

            heap->absoluteMaxSize)

    {

        return true;

    }

    HSTRACE("externalAllocPossible(): "

            "footprint %zu + extAlloc %zu + n %zu >= max %zu (space for %zu)\n",

            currentHeapSize, hs->externalBytesAllocated, n,

            heap->absoluteMaxSize,

            heap->absoluteMaxSize –

                    (currentHeapSize + hs->externalBytesAllocated));

    return false;

}

标为红色的注释的意思应该是说,为了确保我们外部分配内存成功,我们应该保证当前已分配的内存加上当前需要分配的内存值,大小不能超过当前堆的最大内存值,而且内存管理上将外部内存完全当成了当前堆的一部分。也许我们可以这样理解,Bitmap 对象通过栈上的引用来指向堆上的 Bitmap 对象,而 Bitmap 对象又对应了一个使用了外部存储的 native 图像,实际上使用的是 byte[] 来存储的内存空间,如下图:

image

我想到现在大家应该已经对于 Bitmap 内存大小限制有了一个比较清楚的认识了。至于前几天从 Android123 上看到 “Android 的 Btimap 处理大图片解决方法” 一文中提到的使用 BitmapFactory.Options 来设置 inTempStorage 大小,我当时看完之后就尝试了一下,这个设置并不能解决问题,而且很有可能会给你带来不必要的问题。从 BitmapFactory.cpp 中的代码来看,如果 option 不为 null 的话,那么会优先处理 option 中设置的各个参数,假设当前你设置 option 的 inTempStorage 为 1024*1024*4(4M) 大小的话,而且每次解码图像时均使用该 option 对象作为参数,那么你的程序极有可能会提前失败,在我的测试中,我使用了一张大小为 1.03M 的图片来进行解码,如果不使用 option 参数来解码,可以正常解码四次,也就是分配了四次内存,而如果我使用 option 的话,就会出现 OOM 错误,只能正常解码两次不出现 OOM 错误。那么这又是为什么呢?我想是因为这样的,Options 类似与一个预处理参数,当你传入 options 时,并且指定临时使用内存大小的话,Android 将默认先申请你所指定的内存大小,如果申请失败,就抛出 OOM 错误。而如果不指定内存大小,系统将会自动计算,如果当前还剩 3M 空间大小,而我解码只需要 2M 大小,那么在缺省情况下将能解码成功,而在设置 inTempStorage 大小为 4M 的情况下就将出现 OOM 错误。所以,我个人认为通过设置 Options 的 inTempStorage 大小根本不能作为解决大图像解码的方法,而且可能带来不必要的问题,因为 OOM 错误在某些情况是必然出现的,也就是上面我解释的那么多关于堆内存最大值的问题,只要解码需要的内存超过系统可分配的最大内存值,那么 OOM 错误必然会出现。当然对于 Android 开发网 为何发布了这么一篇文章,个人觉得很奇怪,我想作为一个技术人员发布一篇文章,至少应该自己尝试着去测试一下自己的程序吧,如果只是翻翻 SDK 文档,然后就出来一两篇文章声称是解决某问题的方案,恐怕并不是一种负责任的行为吧。

=================================

还是点到为止吧,希望大家都自己去测试一下,验证一下,毕竟自己做过验证的才能算是放心的。

  • 看文章,楼主对自己发表的文章还是蛮负责人的
    顶一个

  • 最近也一直 OOM,因为程序用到和需要显示大量图片,用了 cache 来管理所有的图片,发现一定程度后还是会有 OOM,每次清理我都 recycle 了,难道还要把 cache 设置小。请问下博主看一张图片占用内存看它 bitmap 大小就行了吧。
    我现在怀疑 recycle 是不是真实马上释放了。
    是不是所有对象,资源全部加起来的内存占用只有 16M?

    • 理论上一个图片占用的内存大小就是 bitmap 在内存中的大小,也就是那一大堆 byte 数组以及在栈上的那写引用地址空间。理论上来讲,Android 中的 Activity 每次初始化默认的 VM 堆大小是 16M,频繁的解码图片然后 recycle,会导致内存分布出现碎片,因为在 16M 的空间中,也是使用页面文件来索引的,如果当前页的内存不够当前解码使用,就会查找别的页面,如果查到不到足够的内存,那么就有可能导致 OOM 了,这确实是一个非常让人头疼的问题。对于你的那个所有资源加起来占用内存总大小是否以 16M 为上限,我不是很清楚,不过默认确实有一个堆大小,至于当程序耗尽之后,VM 会不会重新申请到内存,不是很了解。

  • yedeerzi

    非常感谢楼主的分析!
    小弟愚钝,还不理解楼主对于该 OOM 问题的解决方案啊,难道对于大图片就不要解码了?
    请指教!

    • 对于过大的图片,例如分辨率远远超过手机屏幕大小的图片,例如图片大小为 2800*1800 之类的,那么如果直接使用 BitmapFactory.decodeFile(String fileName) 方法来解码的话,那么就非常有可能出先 OOM 问题,相对于这样大小的文件,那么一定需要使用 BitmapFactory.Options 来控制其解码选项,通过设置 BitmapFactory.Options 中的采样率,伸缩率等等参数来将该图片处理成较小的图片,当然这个肯定就会导致图片质量下降了。不过这个也是没有办法的,受限于机器内存以及 Android 框架中的种种限制,编写代码的时候就只能这般了。

  • Pingback: 也扯Android开发 | 7dot9's Laputa()

  • NX

    解决了没有啊。

    • 其实这个问题目前从原则上来说是无解的,只要你的需求就是一次要解的图超过 VM 一次能分配的内存大小就可能出现 OOM 错误,当然我们可以通过一些方法来设置 VM 的初始化堆大小,但是这些都不是解决之道,这篇文章主要是探讨内存的限制,至于如何解决,我给出了一些参考,根据每个人的项目需求都是需要变通了,还是仁者见仁的吧。所以不好意思,我只能说我的问题解决了,如果能帮到你的忙,解决你的问题的话,那就最好了。

  • Simon

    嗯…
    我也會對我發表出來的學習負責,我也是對於發表過的文章,如果說錯了就會去修改的人。
    這個 OutOfMemory 的問題產生的原因很多,
    縱始在外國他們的討論串宣稱某種方式獲得解決,
    但是因為產生的原因真的很多,
    所以他解決,不代表我們也解決。

  • pig345

    这篇文章里的方法,在 G7 等 2.0 以上的机器上可以解决。
    http://jbg168.blog.163.com/blog/static/996836201132161557293/

    InputStream is = this.getResources().openRawResource(R.drawable.pic1);
    Bitmap btp =BitmapFactory.decodeStream(is);

    我怀疑 BitmapFactory.decodeResource(this.getResources(), R.drawable.pic1) 所经常导致的 OOM 其实算是个 BUG。

  • Clark

    首先十分感谢楼主的分享!

    有几个问题需要和楼主探讨一下:
    (1)Android 的 VM 初始化堆大小是不是与屏幕尺寸有一定关联?我记得自己的测试结果是:HVGA(16M)、WVGA(24M)、QVGA(12M)、WXGA(48M)……

    (2)我记得 BitmapFactory.Options 对象中有一个 inSampleSize 的属性,它可以指导 BitmapFactory 在解析图片的时候使用 2 的次幂缩小图片,这样就会节省很多内存,也减少了解析时间。

    • 关于 VM 初始化大小和屏幕大小相关,我认为这是一种必然,其实这个初始化大小无关于屏幕大小,只是在实际的系统定制 ROM 的时候必须考虑到屏幕越大,其需要绘制的图片像素就会增多,那么如果 VM 初始化堆大小还保持 16M 的话将是非常不现实的一种方案,势必会导致很多应用杯具。实际中确实如你所说,跟手机的屏幕大小相关,不过这个并不是必须的,只是一种实际中的应用解决方案,系统定制商在发布之前肯定要做很多的测试最后得出最优的一个基准值,并非所有的 ROM 都是一样的。
      另外关于 BitmapFactory.Options 中的 inSampleSize 其实并不是一个非常好的解决方法,因为 inSampleSize 越大,那么图片的质量丢失越严重,不过我们在实际的应用中,也确实没有那么大的屏幕来在 Android 设备上来显示分辨率非常高的图片,在实际的应用中可以根据屏幕的实际大小以及绘制控件的大小做出调整,因为即便我们按照原图解码出来的图片,最后绘制到一个远小于图片尺寸的控件上,图像的质量还是损失的很严重。所以你讲到的第二个方法也确实可以在实际应用中加以使用。

  • Pingback: Android中解决图像解码导致的OOM问题 | 7dot9's Laputa()

  • Pingback: Android开发中在加载图片时避免OOM的若干方法 « longerian()

  • wang

    楼主请问为什么用 BitmapDrawable 就会明显降低 OOM 出现的次数,这是何故

    • 这位兄台你能否描述一下你的应用场景呢?因为我从代码上来看的话,是看不出来 BitmapDrawable 在降低 OOM 可能性上的任何优势,该对象在构建时同样是使用 BitmapFactory 来进行解码的,内部也没有什么特殊的关于内存的处理,唯一一种可能是 BitmapDrawable 考虑到了 density 的作用,也就是不同分辨率大小时候,系统对于放在 drawable-xxx 目录下的资源文件解码使用的 density 是不一样的,这个倒是有可能会影响到。例如你提供的图片资源都是放置在 drawable-hdpi 中的,然后你在一个 320×480 的分辨率下测试你的应用,就有可能会出现 OOM 频率降低的可能,因为系统会判别你的 density 较低,解码的时候会将其考虑进去,相对其耗费的内存会较少。不过具体你的应用场景是什么因为我不太清楚,可能不能很好地回答你的问题。

  • Pingback: Android开发中处理图片OOM的若干方法小结 | Longerian()

  • 桀骜

    楼主的文章都很好,我看了你的 2 篇关于 OOM 错误的文章,很精辟,让我从根本上知道了解析成 bitmap 时报 OOM 错误的原因,想问问你说的解决 OOM 的一个 2 级数据结构那里的第一级是不是就是指的手机内存?第二级指的软引用。望回复,这个帖子我 MARK 了。会不定时来看看~

  • Pingback: Android开发中处理图片OOM的若干方法小结 - 移动端开发 - 开发者()