Android Bitmap記憶體限制

來源:互聯網
上載者:User

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

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

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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.