Android轉換位圖BUG,知其然不知其所以然


  最近,在開發某App的時候,發現了一個很奇怪的bug,前面我也發了關於bitmap的總結,但是這個問題恰恰出在BitmapFactory.decodeFile(pathName)這個函數上,使用這個函數在我的應用中如果設置在activity的onCreate方法內部,會導致activity無法加載,返回上級activity。

  網上描述的大多數原因是OutOfMemoryError,但我catch不到這個error,所以可以肯定不是內存溢出引起的錯誤。為什么解碼圖像會出現這樣的問題呢?關於這個問題,我糾結了一段時間。

由於調用decodeFile與decodeStream基本相似,中間過程中會引用一個設置bitmap比例的函數外最終都會調用BitmapFactory.decodeStream(is, outPadding, opts),先看一下他們的轉碼流程,下面這段解碼分析參考的是別人一篇oom文章的:

 

  上圖是整個decodeStream實現bitmap轉碼的流程,最終的決定權其實是在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[]來存儲的內存空間,如下圖:

 

 

當然我們承認不好的程序總是程序員自己錯誤的寫法導致的 ,不過我們倒是非常想知道如何來規避這個問題,那么接下來就是解答這個問題的關鍵。

今天無意中看到stackoverflow上有人也曾經遇到過這個問題,而這個給了一個很好的解決方案,但他也不知道這個BUG該怎么解釋:

I had this same issue and solved it by avoiding the BitmapFactory.decodeStream or decodeFile functions and instead used BitmapFactory.decodeFileDescriptor

decodeFileDescriptor looks like it calls different native methods than the decodeStream/decodeFile.

Anyway what worked was this (note that I added some options as some had above, but that's not what made the difference. What is critical is the call to Bitmap.decodeFileDescriptor instead of decodeStream or decodeFile):

 

private void showImage(String path)   {
    Log.i("showImage","loading:"+path);
    BitmapFactory.Options bfOptions=new BitmapFactory.Options();
    bfOptions.inDither=false;                     
    bfOptions.inPurgeable=true;                 
    bfOptions.inInputShareable=true;             
    bfOptions.inTempStorage=new byte[32 * 1024]; 


    File file=new File(path);
    FileInputStream fs=null;
    try {
        fs = new FileInputStream(file);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }

    try {
        if(fs!=null) bm=BitmapFactory.decodeFileDescriptor(fs.getFD(), null, bfOptions);
    } catch (IOException e) {
        e.printStackTrace();
    } finally{ 
        if(fs!=null) {
            try {
                fs.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    im.setImageBitmap(bm);
    bm=null;
}

 

 

 

I think there is a problem with the native function used in decodeStream/decodeFile. I have confirmed that a different native method is called when using decodeFileDescriptor. Also what I've read is "that Images (Bitmaps) are not allocated in a standard Java way but via native calls; the allocations are done outside of the virtual heap, but are counted against it!"

我拙劣的翻譯了一下:

我遇到了同樣的問題,通過規避BitmapFactory.decodeStream或者decodeFile函數,使用BitmapFactory.decodeFileDescriptor解決的decodeFileDescriptor相比decodeStream/decodeFile來說,看起來它調用了不同的本地方法。無論如何,它是這樣工作的(注意,像上邊的一樣,我增加了一些設置,但那不是使這個不同的地方。)關鍵的就是它調用Bitmap.decodeFileDescriptor而不是decodeStream or decodeFile)。

我想這可能是decodeStream/decodeFile中本地函數的問題。我很確定當使用decodeFileDescriptor時一個不同的本地方法被調用。我讀到的也是“圖片(Bitmaps)並不是指派給一個標准的java路徑,但是是通過本地調用的;這個分配是在虛擬的堆外完成的,但是是被認為針對它的!”。

 

還是點到為止吧,大家都應該明白我的題目的意思了,最主要的是這個錯誤沒辦法去驗證究竟什么地方發生了錯誤,但是可以規避這種錯誤,希望大家都自己去測試一下,驗證一下,畢竟自己做過驗證的才能算是放心的。

 

參考資料:http://www.cnblogs.com/-OYK/archive/2012/12/03/2798903.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM