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。