在編寫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文檔,然後就出來一兩篇文章聲稱是解決某問題的方案,恐怕並不是一種負責任的行為吧。
=================================
還是點到為止吧,希望大家都自己去測試一下,驗證一下,畢竟自己做過驗證的才能算是放心的。