转载 Bitmap内存限制问题的一些看法和探索

 

http://www.eoeandroid.com/redirect.php?tid=29144&goto=lastpost#lastpost

http://www.eoeandroid.com/viewthread.php?tid=28774&rpid=227064&ordertype=0&page=1#pid227064

在编写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):     atandroid .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

Your app needs to use less memory.

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

我们从上面的异常堆栈信息中,可以看出是在BitmapFactory.nativeDecodeAsset(),对应该方法的native方法是在BitmapFactory.cpp中的doDecode()方法,在该方法中申请Java PixelAllocator对象时,会调用到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构成)加上我 们需要再次分配的内存大小不能超过堆的最大内存值。那么一个堆的最大内存值究竟是多大呢。通过下面这张图,我们也许可以看到一些线索(自己画的,比较粗 糙)

最终的决定权其实是在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[]来存储的内存空间,如下图:

我想到现在大家应该已经对于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文档,然后就出来一两篇文章声称是解决某问题的方案,恐怕并不是一种负责任的行为吧。

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

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

该问题的解决方法就是谨慎使用Bitmap对象,主要有以下的一些注意点和方法:
1. out of vm budget错误意味着OOM错误被触发了,这个是因为Android对于Bitmap这种对象使用了native解码,内存使用的是external memory也就是这部分内存VM并不会主动进行垃圾回收,需要程序主动释放内存
2. 解决该问题的方法可以有多种,建议使用图片内存池的方式,如果需要显示的图片不多的话,只需要在程序初始化的时候将图片初始化完成并存储到一个数组中,之 后只需要调用数组中的Bitmap对象了,不需要重新解码,因为重新解码会带来新的内存消耗,很容易导致该Out of VM budget问题。
3. 如果需要解码非常多的图片或者数量未知的话,建议仅保存文件名,在每次需要显示时解码需要显示的图片和相邻的图片,建议一个可能需要显示的Bitmap对象链表,当超过一定数量时,就适当地调用recycle()方法回收内存。

也可参考这个帖子
http://www.eoeandroid.com/viewth ... mp;page=1#pid227064

这种需求自然是不需要处理的,但是小图片基类一定的数量,完全是可以导致OOM错误的,其实只要明白一个问题就好了,就是Bitmap的解码是有内存限制的,至于是何种情况会导致限制,就有很多种可能了,可能是单个图片文件太大,也有可能是多个小文件累计内存消耗过大。

是的,之前的一个项目遇到OOM时也研究了下这块,bitmap要么缩放真实size,用完就释放,也没别的更好的方法,android还支持一种size不受限制的bitmap decoder,不过那个是隐藏类,要reflect

1. out of vm budget错误意味着OOM错误被触发了,这个是因为Android对于Bitmap这种对象使用了native解码,内存使用的是external memory也就是这部分内存VM并不会主动进行垃圾回收,需要程序主动释放内存,楼主的代码应该不是在初次运行就会直接崩溃,而是在拖动显示了多次才会 出现该问题。
2. 解决该问题的方法可以有多种,建议使用图片内存池的方式,如果楼主需要显示的图片不多的话,只需要在程序初始化的时候将图片初始化完成并存储到一个数组 中,之后在getView方法中只需要调用数组中的Bitmap对象了,不需要重新解码,因为重新解码会带来新的内存消耗,很容易导致该Out of VM budget问题。
3. 如果楼主需要解码非常多的图片或者数量未知的话,建议仅保存文件名,在每次getView中解码需要显示的图片和相邻的图片,建议一个可能需要显示的Bitmap对象链表,当超过一定数量时,就适当地调用recycle()方法回收内存。
=============================================
楼主的这个问题在模拟器上不出现是因为楼主模拟器上的VM Budget比较大,而真机VM Budget较模拟器小,如果楼主每次显示都重新去解码图片,而没有做任何回收的话,这个问题肯定会出现,只是图片数量的问题,因为外部内存有限,非常容易就会出现这个问题。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值