Bitmap內存回收機制 Android 2.3.3以前 Android 3.0~Android 7.1 Android 8.0之後

Bitmap可以說是安卓裏面最常見的內存消耗大戶了,我們開發過程中遇到的oom問題很多都是由它引發的。谷歌官方也一直在迭代它的像素內存管理策略。從 Android 2.3.3以前的分配在native上,到2.3-7.1之間的分配在java堆上,又到8.0之後的回到native上。幾度變遷,它的回收方法也在跟着變化。

Android 2.3.3以前

2.3.3以前Bitmap的像素內存是分配在natvie上,而且不確定什麼時候會被回收。根據官方文檔的說法我們需要手動調用Bitmap.recycle()去回收:

在 Android 2.3.3(API 級別 10)及更低版本上,位圖的後備像素數據存儲在本地內存中。它與存儲在 Dalvik 堆中的位圖本身是分開的。本地內存中的像素數據並不以可預測的方式釋放,可能會導致應用短暫超出其內存限制並崩潰。

在 Android 2.3.3(API 級別 10)及更低版本上,建議使用 recycle()。如果您在應用中顯示大量位圖數據,則可能會遇到 OutOfMemoryError 錯誤。利用 recycle() 方法,應用可以儘快回收內存。

注意:只有當您確定位圖已不再使用時才應該使用 recycle()。如果您調用 recycle() 並在稍後嘗試繪製位圖,則會收到錯誤:"Canvas: trying to use a recycled bitmap"

Android 3.0~Android 7.1

雖然3.0~7.1的版本Bitmp的像素內存是分配在java堆上的,但是實際是在natvie層進行decode的,而且會在native層創建一個c++的對象和java層的Bitmap對象進行關聯。

從BitmapFactory的源碼我們可以看到它一路調用到nativeDecodeStream這個native方法:

// BitmapFactory.java
public static Bitmap decodeFile(String pathName, Options opts) {
    ...
    stream = new FileInputStream(pathName);
    bm = decodeStream(stream, null, opts);
    ...
    return bm;
}

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
    ...
    bm = decodeStreamInternal(is, outPadding, opts);
    ...
    return bm;
}

private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) {
    ...
    return nativeDecodeStream(is, tempStorage, outPadding, opts);
}

nativeDecodeStream實際上會通過jni創建java堆的內存,然後讀取io流解碼圖片將像素數據存到這個java堆內存裏面:


// BitmapFactory.cpp
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
        jobject padding, jobject options) {
    ...
    bitmap = doDecode(env, bufferedStream, padding, options);
    ...
    return bitmap;
}

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
    ...
    // outputAllocator是像素內存的分配器,會在java堆上創建內存給像素數據,可以通過BitmapFactory.Options.inBitmap複用前一個bitmap像素內存
    SkBitmap::Allocator* outputAllocator = (javaBitmap != NULL) ?
            (SkBitmap::Allocator*)&recyclingAllocator : (SkBitmap::Allocator*)&javaAllocator;
    ...
    // 將內存分配器設置給解碼器
    decoder->setAllocator(outputAllocator);
    ...
    //解碼
    if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode)
                != SkImageDecoder::kSuccess) {
        return nullObjectReturn("decoder->decode returned false");
    }
    ...
    return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
            bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}

// Graphics.cpp
jobject GraphicsJNI::createBitmap(JNIEnv* env, android::Bitmap* bitmap,
        int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
        int density) {

    // java層的Bitmap對象實際上是natvie層new出來的
    // native層也會創建一個android::Bitmap對象與java層的Bitmap對象綁定
    // bitmap->javaByteArray()代碼bitmap的像素數據其實是存在java層的byte數組中
    jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
            reinterpret_cast<jlong>(bitmap), bitmap->javaByteArray(),
            bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied,
            ninePatchChunk, ninePatchInsets);
    ...
    return obj;
}

我們可以看最後會調用javaAllocator.getStorageObjAndReset()創建一個android::Bitmap類型的native層Bitmap對象,然後通過jni調用java層的Bitmap構造函數去創建java層的Bitmap對象,同時將native層的Bitmap對象保存到mNativePtr:


// Bitmap.java
// Convenience for JNI access
private final long mNativePtr;

/**
 * Private constructor that must received an already allocated native bitmap
 * int (pointer).
 */
// called from JNI
Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
        boolean isMutable, boolean requestPremultiplied,
        byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
    ...
    mNativePtr = nativeBitmap;
    ...
}

從上面的源碼我們也能看出來,Bitmap的像素是存在java堆的,所以如果bitmap沒有人使用了,垃圾回收器就能自動回收這塊的內存,但是在native創建出來的nativeBitmap要怎麼回收呢?從Bitmap的源碼我們可以看到在Bitmap構造函數裏面還會創建一個BitmapFinalizer去管理nativeBitmap:

/**
 * Private constructor that must received an already allocated native bitmap
 * int (pointer).
 */
// called from JNI
Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
        boolean isMutable, boolean requestPremultiplied,
        byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
    ...
    mNativePtr = nativeBitmap;
    mFinalizer = new BitmapFinalizer(nativeBitmap);
    ...
}

BitmapFinalizer的原理十分簡單。Bitmap對象被銷燬的時候BitmapFinalizer也會同步被銷燬,然後就可以在BitmapFinalizer.finalize()裏面銷燬native層的nativeBitmap:

private static class BitmapFinalizer {
    private long mNativeBitmap;
    ...
    BitmapFinalizer(long nativeBitmap) {
        mNativeBitmap = nativeBitmap;
    }
    ...
    @Override
    public void finalize() {
        try {
            super.finalize();
        } catch (Throwable t) {
            // Ignore
        } finally {
            setNativeAllocationByteCount(0);
            nativeDestructor(mNativeBitmap);
            mNativeBitmap = 0;
        }
    }
}

Android 8.0之後

8.0以後像素內存又被放回了native上,所以依然需要在java層的Bitmap對象回收之後同步回收native的內存。

雖然BitmapFinalizer同樣可以實現,但是Java的finalize方法實際上是不推薦使用的,所以谷歌也換了NativeAllocationRegistry去實現:

/**
 * Private constructor that must received an already allocated native bitmap
 * int (pointer).
 */
// called from JNI
Bitmap(long nativeBitmap, int width, int height, int density,
        boolean isMutable, boolean requestPremultiplied,
    ...
    mNativePtr = nativeBitmap;
    long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
    NativeAllocationRegistry registry = new NativeAllocationRegistry(
        Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
    registry.registerNativeAllocation(this, nativeBitmap);
}

NativeAllocationRegistry底層實際上使用了sun.misc.Cleaner,可以爲對象註冊一個清理的Runnable。當對象內存被回收的時候jvm就會調用它。

import sun.misc.Cleaner;

public Runnable registerNativeAllocation(Object referent, Allocator allocator) {
    ...
    CleanerThunk thunk = new CleanerThunk();
    Cleaner cleaner = Cleaner.create(referent, thunk);
    ..
}

private class CleanerThunk implements Runnable {
    ...
    public void run() {
        if (nativePtr != 0) {
            applyFreeFunction(freeFunction, nativePtr);
        }
        registerNativeFree(size);
    }
    ...
}

這個Cleaner的原理也很暴力,首先它是一個虛引用,registerNativeAllocation實際上創建了一個Bitmap的虛引用:

// Cleaner.java
public class Cleaner extends PhantomReference {
    ...
    public static Cleaner create(Object ob, Runnable thunk) {
        ...
        return add(new Cleaner(ob, thunk));
    }
    ...
    private Cleaner(Object referent, Runnable thunk) {
        super(referent, dummyQueue);
        this.thunk = thunk;
    }
    ...
    public void clean() {
        ...
        thunk.run();
        ...
    }
    ...
}

虛引用的話我們都知道需要配合一個ReferenceQueue使用,當對象的引用被回收的時候,jvm就會將這個虛引用丟到ReferenceQueue裏面。而ReferenceQueue在插入的時候居然通過instanceof判斷了下是不是Cleaner:

// ReferenceQueue.java
private boolean enqueueLocked(Reference<? extends T> r) {
    ...
    if (r instanceof Cleaner) {
        Cleaner cl = (sun.misc.Cleaner) r;
        cl.clean();
        ...
    }
    ...
}

也就是說Bitmap對象被回收,就會觸發Cleaner這個虛引用被丟入ReferenceQueue,而ReferenceQueue裏面會判斷丟進來的虛引用是不是Cleaner,如果是就調用Cleaner.clean()方法。而clean方法內部就會再去執行我們註冊的清理的Runnable。

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