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 3405,Issue 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()方法,在该方法中申请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构成)加上我们需要再次分配的内存大小不能超过堆的最大内存值。那么一个堆的最大内存值究竟是多大呢。通过下面这张图,我们也许可以看到一些线索(自己画的,比较粗糙)

最终的决定权其实是在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文档,然后就出来一两篇文章声称是解决某问题的方案,恐怕并不是一种负责任的行为吧。

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

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

更多相关文章

  1. Android的内存管理机制
  2. Android(安卓)Java basic knowledge ---AIDL3
  3. Android(安卓)Studio报错Manifest merger failed with multiple
  4. Android退出时关闭所有Activity的方法
  5. android独有的省内存的轻量级容器类
  6. Android的事件转递机制
  7. Android中判断Intent是否存在的方法
  8. 浅谈Java中Collections.sort对List排序的两种方法
  9. Python list sort方法的具体使用

随机推荐

  1. golang语言学习之什么是golang
  2. docker修改默认指定路径
  3. LEADTOOLS HTML5 JavaScript控件入门介绍
  4. 我的第十个代码
  5. 几种Linux系统切换内核启动顺序方法
  6. PHP基础: 字符串函数
  7. 我的第13个代码
  8. 三十而已的年纪,三十真的可以还可以转行吗
  9. 我的第12个代码
  10. 航电oj2006-2008参考代码