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。