Android O Bitmap 內存分配


  我們知道,一般認為在Android進程的內存模型中,heap分為兩部分,一部分是native heap,一部分是Dalvik heap(實際上也是native heap的一部分)。

  Android Bitmap 是一個比較特殊的類,用來加載圖片的,而圖片的數據部分一般較大,因此在創建Bitmap對象時,Android system 采用的策略是將其分為兩個部分,一個是基本信息(如寬度),一個是像素點數據。前者會保存在Dalvik heap中,也就是Bitmap對象所指的空間,后者會單獨放一個內存空間里,按照不同的Android系統版本,會放在不同的heap中。

  我們先引用一段Android官方的說法:鏈接

On Android 2.3.3 (API level 10) and lower, the backing pixel data for a bitmap is stored in native memory. It is separate from the bitmap itself, which is stored in the Dalvik heap. The pixel data in native memory is not released in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash. As of Android 3.0 (API level 11), the pixel data is stored on the Dalvik heap along with the associated bitmap.

  Android 2.3.3及以前版本,像素點數據是保存在native memory,而bitmap對象是保存在Dalvik heap. 從Android 3.0開始,像素點數據與bitmap對象一起存儲在Dalvik heap中。

  但其實按目前來看,官方的說法並不全面,可能是未能及時更新。問題起源於我在項目里做的一個功能。該功能會創建若干個中間Bitmap對象,這些對象都是局部變量,並且在使用過一次之后就不會再用到。但bitmap占用的空間較大,需要考慮到內存問題,其自身提供了recycle方法,每次用完后是否需要主動調用該方法呢?我想這是個問題,所以需要驗證下沒調用recycle方法會不會導致內存泄露。

  於是我使用MAT來觀察內存的使用情況。發現在GC后,沒能找到這幾個中間bitmap對象的引用,但由於在驗證的時候,會有一個其它界面會創建較多的bitmap,我擔心會影響我的排查。於是寫了個demo驗證官方的說法。按道理,我們的應用是基於Android O開發的,應該是符合官網說的“像素點數據與bitmap對象一起存儲在Dalvik heap中”, 而且局部變量會很快地被回收,理論上不應該有內存泄露。

demo1

    void load() {
        for (int i = 0; i < 100; i++) {
            Bitmap bitmaps = BitmapFactory.decodeFile(path);
        }
    }

  通過AS3.0的Android Profiler觀察,發現情況有些出乎意料。

  代碼中重復加載了100次的圖片,這個圖片的源文件大小大概3MB多,100次循環后,Native 竟然飆升到1.26GB, 應用正常運行,並不會OOM,而Java Heap基本上沒變,大概是3M多,由於顯示的單位切換成了GB,Java那一欄只能顯示到小數點后1位,因此3MB最后顯示出來是0。

  為了解開這個出乎意料的結果,我們需要從源碼找答案。

  跟蹤BitmapFactory.decodeFile(path)方法,最后會調用到nativeDecodeStream方法,該方法對應BitmapFactory.cpp文件中的nativeDecodeStream函數。

static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
        jobject padding, jobject options) {

    jobject bitmap = NULL;
    std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));

    if (stream.get()) {
        std::unique_ptr<SkStreamRewindable> bufferedStream(
                SkFrontBufferedStream::Create(stream.release(), SkCodec::MinBufferedBytesNeeded()));
        SkASSERT(bufferedStream.get() != NULL);
        bitmap = doDecode(env, bufferedStream.release(), padding, options);
    }
    return bitmap;
}

  然后再調用 doDecode函數,由於該函數的代碼非常長,我這里只貼出與本文相關的比較重要的代碼。

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
    HeapAllocator defaultAllocator;
    RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize);
    ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize);
    SkBitmap::HeapAllocator heapAllocator;
    SkBitmap::Allocator* decodeAllocator;
    if (javaBitmap != nullptr && willScale) {
        decodeAllocator = &scaleCheckingAllocator;
    } else if (javaBitmap != nullptr) {
        decodeAllocator = &recyclingAllocator;
    } else if (willScale || isHardware) {
        decodeAllocator = &heapAllocator;
    } else {
        decodeAllocator = &defaultAllocator;
    }

    SkBitmap decodingBitmap;
    if (!decodingBitmap.setInfo(bitmapInfo) ||
            !decodingBitmap.tryAllocPixels(decodeAllocator, colorTable.get())) {
        return nullptr;
    }
   
    return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
            bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}

  可見,通過tryAllocPixels嘗試分配空間,默認采用的是defaultAllocator內存分配器,它的類型是HeapAllocator。

  decodingBitmap.tryAllocPixels函數實際會調用defaultAllocator->allocPixelRef,該函數代碼如下

bool HeapAllocator::allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) {
    mStorage = android::Bitmap::allocateHeapBitmap(bitmap, ctable);
    return !!mStorage;
}

  只是簡單的調用了android::Bitmap::allocateHeapBitmap,而這個函數是在另一個庫下面的(frameworks/base/libs/hwui/hwui/Bitmap.cpp,找了很久才找到)

static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes,
        SkColorTable* ctable) {
    void* addr = calloc(size, 1);
    if (!addr) {
        return nullptr;
    }
    return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes, ctable));
}

  最終調用的是calloc函數,該函數和malloc是類似,都是直接在native heap上分配空間,返回地址。

  所以結論是:Android O上通過BitmapFactory.decodeFile方法創建的Bitmap,其中的像素點數據集默認在native heap上分配的。

  但是官方為什么會說“像素點數據與bitmap對象一起存儲在Dalvik heap中”,我想可能是Android O 改了,然后未及時更新這段文字,因此我們基於Android N再來驗證一下。

  同樣使用demo1的代碼,在Android N(7.1.1)的機器上運行,得到如下結果:

  看起來正常了,符合官方說法,為了確定Android O確實修改了分配Bitmap內存的相關代碼,我們來看看Android N的源碼。

  BitmapFactory.decode函數。

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
    JavaPixelAllocator javaAllocator(env);
    RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize);
    ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize);
    SkBitmap::HeapAllocator heapAllocator;
    SkBitmap::Allocator* decodeAllocator;
    if (javaBitmap != nullptr && willScale) {
        decodeAllocator = &scaleCheckingAllocator;
    } else if (javaBitmap != nullptr) {
        decodeAllocator = &recyclingAllocator;
    } else if (willScale) {
        decodeAllocator = &heapAllocator;
    } else {
        decodeAllocator = &javaAllocator;
    }
    
    SkBitmap decodingBitmap;
    if (!decodingBitmap.setInfo(bitmapInfo) ||
            !decodingBitmap.tryAllocPixels(decodeAllocator, colorTable)) {
        return nullptr;
    }

    return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
            bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}

  我們看到默認使用的分配器是JavaPixelAllocator,官方對這個分配器的解釋如下,其實已經說得很清楚了,這個分配器就是在java heap中進行內存分配。

/** Allocator which allocates the backing buffer in the Java heap.

  • Instances can only be used to perform a single allocation, which helps
  • ensure that the allocated buffer is properly accounted for with a
  • reference in the heap (or a JNI global reference).
    */

  接着看JavaPixelAllocator::allocPixelRef。

bool JavaPixelAllocator::allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) {
    JNIEnv* env = vm2env(mJavaVM);

    mStorage = GraphicsJNI::allocateJavaPixelRef(env, bitmap, ctable);
    return mStorage != nullptr;
}

  再看GraphicsJNI::allocateJavaPixelRef。

android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap,
                                             SkColorTable* ctable) {
    const size_t rowBytes = bitmap->rowBytes();

    jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime,
                                                             gVMRuntime_newNonMovableArray,
                                                             gByte_class, size);

    jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj);
    if (env->ExceptionCheck() != 0) {
        return NULL;
    }

    android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr,
            info, rowBytes, ctable);
    wrapper->getSkBitmap(bitmap);
    bitmap->lockPixels();
    return wrapper;
}

  我們看到,實際是通過java層進行內存分配,調用了gVMRuntime的gVMRuntime_newNonMovableArray,得到一個字節數組,再調用gVMRuntime_addressOf得到這個數組的地址,然后將地址作為android::Bitmat構造函數參數創建android::Bitma對象,返回該對象。實際上java層的Bitmap對象會有一個long型成員變量保存native的這個Bitmap對象的引用。接着看下具體調用哪個方法。

    c = env->FindClass("java/lang/Byte");
    gByte_class = (jclass) env->NewGlobalRef(
        env->GetStaticObjectField(c, env->GetStaticFieldID(c, "TYPE", "Ljava/lang/Class;")));

    gVMRuntime_class = make_globalref(env, "dalvik/system/VMRuntime");
    m = env->GetStaticMethodID(gVMRuntime_class, "getRuntime", "()Ldalvik/system/VMRuntime;");
    gVMRuntime = env->NewGlobalRef(env->CallStaticObjectMethod(gVMRuntime_class, m));
    gVMRuntime_newNonMovableArray = env->GetMethodID(gVMRuntime_class, "newNonMovableArray",
                                                     "(Ljava/lang/Class;I)Ljava/lang/Object;");
    gVMRuntime_addressOf = env->GetMethodID(gVMRuntime_class, "addressOf", "(Ljava/lang/Object;)J");

  通過java層的dalvik/system/VMRuntime類的靜態方法getRuntime獲取一個VMRuntime的實例gVMRuntime,然后調用newNonMovableArray方法獲取一個字節數組,最后調用addressOf獲取這個字節數組第1個元素(array[0])的地址。實際上newNonMovableArray方法最終也是要調用native方法進行內存分配的,具體調用的是dalvik_system_VMRuntime::VMRuntime_newNonMovableArray函數。最后會通過heap實例,分配一個內存。前面提到,dalvik heap也是native heap的一部分。是因為在啟動dalvik vm的時候,會預先在native heap中分配一段內存作為dalvik heap使用,后續java層如果需要請求內存,都會在這個dalvik heap中進行分配,如果dalvik heap空間不夠,就先進行GC,GC后如果還不夠就會再分配一個更大的空間,如果已經達到上限,就會拋出OOM異常。

  Android N 上Bitmap的像素點數據與bitmap對象都是分配到dalvik heap,而Android O 上Bitmap的像素點數據是分配在native heap中,因此在Android O加載大量的Bitmap並不會導致應用OOM,但是有一點要注意,android O對應用native使用的空間也做了限制(不確定是O新增的還是原來就有),當應用占用的native空間到一定程度時(我本地驗證是1.26G),再調用BitmapFactory.decodeFile()方法時,會直接返回null。所以Android O對Bitmap內存分配進行了更新,這對開發者來說其實不影響。在需要加載大量Bitmap的時候,該優化還是要優化,該緩存還是要緩存。只是對於某些將Bitmap通過JNI方式直接在native請求空間的優化方案來說,就失去意義了。


免責聲明!

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



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