Android Bitmap內存限制

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

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構成)加上我們需要再次分配的內存大小不能超過堆的最大內存值。那麼一個堆的最大內存值究竟是多大呢。通過下面這張圖,我們也許可以看到一些線索(自己畫的,比較粗糙)

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文檔,然後就出來一兩篇文章聲稱是解決某問題的方案,恐怕並不是一種負責任的行爲吧。

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

還是點到爲止吧,希望大家都自己去測試一下,驗證一下,畢竟自己做過驗證的才能算是放心的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章