Bitmap內(nèi)存回收機(jī)制

Bitmap可以說是安卓里面最常見的內(nèi)存消耗大戶了嗦嗡,我們開發(fā)過程中遇到的oom問題很多都是由它引發(fā)的模她。谷歌官方也一直在迭代它的像素內(nèi)存管理策略咐蝇。從 Android 2.3.3以前的分配在native上授帕,到2.3-7.1之間的分配在java堆上特姐,又到8.0之后的回到native上晶丘。幾度變遷,它的回收方法也在跟著變化唐含。

Android 2.3.3以前

2.3.3以前Bitmap的像素內(nèi)存是分配在natvie上浅浮,而且不確定什么時(shí)候會(huì)被回收。根據(jù)官方文檔的說法我們需要手動(dòng)調(diào)用Bitmap.recycle()去回收:

在 Android 2.3.3(API 級(jí)別 10)及更低版本上捷枯,位圖的后備像素?cái)?shù)據(jù)存儲(chǔ)在本地內(nèi)存中滚秩。它與存儲(chǔ)在 Dalvik 堆中的位圖本身是分開的。本地內(nèi)存中的像素?cái)?shù)據(jù)并不以可預(yù)測(cè)的方式釋放淮捆,可能會(huì)導(dǎo)致應(yīng)用短暫超出其內(nèi)存限制并崩潰郁油。

在 Android 2.3.3(API 級(jí)別 10)及更低版本上,建議使用 recycle()攀痊。如果您在應(yīng)用中顯示大量位圖數(shù)據(jù)桐腌,則可能會(huì)遇到 OutOfMemoryError 錯(cuò)誤。利用 recycle() 方法苟径,應(yīng)用可以盡快回收內(nèi)存哩掺。

注意:只有當(dāng)您確定位圖已不再使用時(shí)才應(yīng)該使用 recycle()。如果您調(diào)用 recycle() 并在稍后嘗試?yán)L制位圖涩笤,則會(huì)收到錯(cuò)誤:"Canvas: trying to use a recycled bitmap"嚼吞。

Android 3.0~Android 7.1

雖然3.0~7.1的版本Bitmp的像素內(nèi)存是分配在java堆上的,但是實(shí)際是在natvie層進(jìn)行decode的蹬碧,而且會(huì)在native層創(chuàng)建一個(gè)c++的對(duì)象和java層的Bitmap對(duì)象進(jìn)行關(guān)聯(lián)舱禽。

從BitmapFactory的源碼我們可以看到它一路調(diào)用到nativeDecodeStream這個(gè)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實(shí)際上會(huì)通過jni創(chuàng)建java堆的內(nèi)存,然后讀取io流解碼圖片將像素?cái)?shù)據(jù)存到這個(gè)java堆內(nèi)存里面:


// 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是像素內(nèi)存的分配器,會(huì)在java堆上創(chuàng)建內(nèi)存給像素?cái)?shù)據(jù),可以通過BitmapFactory.Options.inBitmap復(fù)用前一個(gè)bitmap像素內(nèi)存
    SkBitmap::Allocator* outputAllocator = (javaBitmap != NULL) ?
            (SkBitmap::Allocator*)&recyclingAllocator : (SkBitmap::Allocator*)&javaAllocator;
    ...
    // 將內(nèi)存分配器設(shè)置給解碼器
    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對(duì)象實(shí)際上是natvie層new出來(lái)的
    // native層也會(huì)創(chuàng)建一個(gè)android::Bitmap對(duì)象與java層的Bitmap對(duì)象綁定
    // bitmap->javaByteArray()代碼bitmap的像素?cái)?shù)據(jù)其實(shí)是存在java層的byte數(shù)組中
    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;
}

我們可以看最后會(huì)調(diào)用javaAllocator.getStorageObjAndReset()創(chuàng)建一個(gè)android::Bitmap類型的native層Bitmap對(duì)象,然后通過jni調(diào)用java層的Bitmap構(gòu)造函數(shù)去創(chuàng)建java層的Bitmap對(duì)象恩沽,同時(shí)將native層的Bitmap對(duì)象保存到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;
    ...
}

從上面的源碼我們也能看出來(lái)誊稚,Bitmap的像素是存在java堆的,所以如果bitmap沒有人使用了罗心,垃圾回收器就能自動(dòng)回收這塊的內(nèi)存里伯,但是在native創(chuàng)建出來(lái)的nativeBitmap要怎么回收呢?從Bitmap的源碼我們可以看到在Bitmap構(gòu)造函數(shù)里面還會(huì)創(chuàng)建一個(gè)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的原理十分簡(jiǎn)單渤闷。Bitmap對(duì)象被銷毀的時(shí)候BitmapFinalizer也會(huì)同步被銷毀疾瓮,然后就可以在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以后像素內(nèi)存又被放回了native上,所以依然需要在java層的Bitmap對(duì)象回收之后同步回收native的內(nèi)存飒箭。

雖然BitmapFinalizer同樣可以實(shí)現(xiàn)狼电,但是Java的finalize方法實(shí)際上是不推薦使用的,所以谷歌也換了NativeAllocationRegistry去實(shí)現(xiàn):

/**
 * 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底層實(shí)際上使用了sun.misc.Cleaner,可以為對(duì)象注冊(cè)一個(gè)清理的Runnable弦蹂。當(dāng)對(duì)象內(nèi)存被回收的時(shí)候jvm就會(huì)調(diào)用它肩碟。

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);
    }
    ...
}

這個(gè)Cleaner的原理也很暴力,首先它是一個(gè)虛引用,registerNativeAllocation實(shí)際上創(chuàng)建了一個(gè)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();
        ...
    }
    ...
}

虛引用的話我們都知道需要配合一個(gè)ReferenceQueue使用,當(dāng)對(duì)象的引用被回收的時(shí)候凸椿,jvm就會(huì)將這個(gè)虛引用丟到ReferenceQueue里面削祈。而ReferenceQueue在插入的時(shí)候居然通過instanceof判斷了下是不是Cleaner:

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

也就是說Bitmap對(duì)象被回收,就會(huì)觸發(fā)Cleaner這個(gè)虛引用被丟入ReferenceQueue,而ReferenceQueue里面會(huì)判斷丟進(jìn)來(lái)的虛引用是不是Cleaner脑漫,如果是就調(diào)用Cleaner.clean()方法髓抑。而clean方法內(nèi)部就會(huì)再去執(zhí)行我們注冊(cè)的清理的Runnable。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末窿撬,一起剝皮案震驚了整個(gè)濱河市启昧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌劈伴,老刑警劉巖密末,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異跛璧,居然都是意外死亡严里,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門追城,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)刹碾,“玉大人,你說我怎么就攤上這事座柱∶灾模” “怎么了物舒?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)戏锹。 經(jīng)常有香客問我冠胯,道長(zhǎng),這世上最難降的妖魔是什么锦针? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任荠察,我火速辦了婚禮,結(jié)果婚禮上奈搜,老公的妹妹穿的比我還像新娘悉盆。我一直安慰自己,他們只是感情好馋吗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布焕盟。 她就那樣靜靜地躺著,像睡著了一般耗美。 火紅的嫁衣襯著肌膚如雪京髓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天商架,我揣著相機(jī)與錄音堰怨,去河邊找鬼。 笑死蛇摸,一個(gè)胖子當(dāng)著我的面吹牛备图,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播赶袄,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼揽涮,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了饿肺?” 一聲冷哼從身側(cè)響起蒋困,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎敬辣,沒想到半個(gè)月后雪标,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡溉跃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年村刨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撰茎。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡嵌牺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情逆粹,我是刑警寧澤募疮,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站僻弹,受9級(jí)特大地震影響酝锅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜奢方,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望爸舒。 院中可真熱鬧蟋字,春花似錦、人聲如沸扭勉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)涂炎。三九已至忠聚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間唱捣,已是汗流浹背两蟀。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留震缭,地道東北人赂毯。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像拣宰,于是被迫代替她去往敵國(guó)和親党涕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容