0%

Bitmap的回收

平时经常使用Bitmap,但对Bitmap的生命周期没有仔细思考过,最近查看源码时突然想到一个问题:Bitmap是怎么回收的?Java堆上的android.graphics.Bitmap只是描述了位图的轮廓,真正的像素信息保存在了native 堆上,那么这块内存是怎么回收的呢?我们没有特意去关心这部分内存,只是将android.graphics.Bitmap置为空就结束了,那一定是系统帮我们完成了回收。

Bitmap的创建

Android提供BitmapFactory创建Bitmap,不管使用哪个静态方法,最后都会调到native代码去真正解码图片并将得到的信息转换成位图信息,调用栈如下:

1
2
3
4
-> frameworks/base/graphics/java/android/graphics/BitmapFactory.java decodeStream()
-> frameworks/base/graphics/java/android/graphics/BitmapFactory.java nativeDecodeAsset()
-> frameworks/base/libs/hwui/jni/BitmapFactory.cpp nativeDecodeAsset()
-> frameworks/base/libs/hwui/jni/BitmapFactory.cpp doDecode()

在 doDecode方法中解码图片得到位图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream,
jobject padding, jobject options, jlong inBitmapHandle,
jlong colorSpaceHandle) {
/**
省略的代码主要工作:
1、如果只是加载宽高返回
2、如果有复用的Bitmap则复用
3、计算缩放比例
**/

//分配native内存
SkBitmap decodingBitmap;
if (!decodingBitmap.setInfo(bitmapInfo) ||
!decodingBitmap.tryAllocPixels(decodeAllocator)) {
// SkAndroidCodec should recommend a valid SkImageInfo, so setInfo()
// should only only fail if the calculated value for rowBytes is too
// large.
// tryAllocPixels() can fail due to OOM on the Java heap, OOM on the
// native heap, or the recycled javaBitmap being too small to reuse.
return nullptr;
}

// Use SkAndroidCodec to perform the decode.
SkAndroidCodec::AndroidOptions codecOptions;
codecOptions.fZeroInitialized = decodeAllocator == &defaultAllocator ?
SkCodec::kYes_ZeroInitialized : SkCodec::kNo_ZeroInitialized;
codecOptions.fSampleSize = sampleSize;

//调用解码器解码图片,转换的位图信息保存到decodingBitmap.getPixels()得到的地址中
SkCodec::Result result = codec->getAndroidPixels(decodeInfo, decodingBitmap.getPixels(),
decodingBitmap.rowBytes(), &codecOptions);

/**
省略的代码主要工作:
1、如果有缩放,创建缩放的位图
2、如果有复用的Bitmap,直接使用它创建java Bitmap
3、如果是硬件位图,走对应的逻辑
**/

// now create the java bitmap
return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);

}

doDecode代码很长,这里只列出了本文最关心的代码,在代码的最后创建了java Bitmap,我们再看看Java Bitmap对象的创建过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

// called from JNI and Bitmap_Delegate.
Bitmap(long nativeBitmap, int width, int height, int density,
boolean requestPremultiplied, byte[] ninePatchChunk,
NinePatch.InsetStruct ninePatchInsets, boolean fromMalloc) {
if (nativeBitmap == 0) {
throw new RuntimeException("internal error: native bitmap is 0");
}

mWidth = width;
mHeight = height;
mRequestPremultiplied = requestPremultiplied;

mNinePatchChunk = ninePatchChunk;
mNinePatchInsets = ninePatchInsets;
if (density >= 0) {
mDensity = density;
}

mNativePtr = nativeBitmap;

final int allocationByteCount = getAllocationByteCount();
NativeAllocationRegistry registry;
if (fromMalloc) {//native内存分配
registry = NativeAllocationRegistry.createMalloced(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount);
} else {//mmap内存分配
registry = NativeAllocationRegistry.createNonmalloced(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount);
}
registry.registerNativeAllocation(this, nativeBitmap);
}

前部分代码很简单,主要就是保存了我们在native层创建的bitmap对象的地址,比较新奇的是新建的NativeAllocationRegistry这个对象,它是做什么的呢?其实就是通过它实现静悄悄地回收native Bitmap。

关于NativeAllocationRegistry介绍看这篇文章:ART虚拟机 | 如何让GC同步回收native内存,总结下来在Bitmap构造器中NativeAllocationRegistry就干了三件事:

  1. 将本次分配的native内存也统计进Java虚拟机,让虚拟机尽可能多的统计分配给Java相关的内存
  2. 综合这次分配的内存大小、上次native总内存的大小等因素判断下是否立即执行一次java GC
  3. 为这个java Bitmap创建一个幽灵引用(PhantomReference),方便监听它被回收后立马回收native Bitmap内存

Bitmap的回收

Android N API Level 25之后

上面讲到的NativeAllocationRegistry就是在Android 7加入到Bitmap的,在第三点提到创建了一个幽灵引用,实际上是sun.misc.Cleaner实例,它是PhantomReference的子类,创建的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
libcore/luni/src/main/java/libcore/util/NativeAllocationRegistry.java

//Bitmap调用的静态方法
public static NativeAllocationRegistry createMalloced(
@NonNull ClassLoader classLoader, long freeFunction, long size) {
return new NativeAllocationRegistry(classLoader, freeFunction, size, true);
}

//先调用到构造器,保存native层释放内存的方法的地址
private NativeAllocationRegistry(ClassLoader classLoader, long freeFunction, long size,
boolean mallocAllocation) {
if (size < 0) {
throw new IllegalArgumentException("Invalid native allocation size: " + size);
}
this.classLoader = classLoader;
//保存native层释放内存的方法的地址
this.freeFunction = freeFunction;
this.size = mallocAllocation ? (size | IS_MALLOCED) : (size & ~IS_MALLOCED);
}

//后调用到这个方法
public @NonNull Runnable registerNativeAllocation(@NonNull Object referent, long nativePtr) {
if (referent == null) {
throw new IllegalArgumentException("referent is null");
}
if (nativePtr == 0) {
throw new IllegalArgumentException("nativePtr is null");
}

CleanerThunk thunk;
CleanerRunner result;
try {
thunk = new CleanerThunk();//被回收时执行clean的Runnable
Cleaner cleaner = Cleaner.create(referent, thunk);
result = new CleanerRunner(cleaner);//返回给调用方的Runnable,用于调用方主动执行clean
registerNativeAllocation(this.size);
} catch (VirtualMachineError vme /* probably OutOfMemoryError */) {
applyFreeFunction(freeFunction, nativePtr);
throw vme;
} // Other exceptions are impossible.
// Enable the cleaner only after we can no longer throw anything, including OOME.
//赋值native对应内存的地址
thunk.setNativePtr(nativePtr);
// Ensure that cleaner doesn't get invoked before we enable it.
Reference.reachabilityFence(referent);
return result;
}

registerNativeAllocation是关键,它创建了Cleaner对象,关于Cleaner的进一步介绍见这篇文章:ART虚拟机 | Finalize的替代者Cleaner,总结这篇文章,sun.misc.Cleaner其实是行为比较特殊的PhantomReference,特殊在于:

  1. 在它关联的Java对象被释放时,不同于PhantomReference要入队到队列,它并没有真正入队到某个队列
  2. 执行释放内存的方法,在remove()方法中执行了调用了释放native内存的方法

Android N API Level 25之前

Android 7之前,Bitmap的回收时依赖于Object的finalize方法

1
2
3
4
5
6
7
8
9
10
11
12
//android 6 frameworks/base/graphics/java/android/graphics/Bitmap.java
//构造器
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);
int nativeAllocationByteCount = (buffer == null ? getByteCount() : 0);
mFinalizer.setNativeAllocationByteCount(nativeAllocationByteCount);
}

实际工作的BitmapFinalizer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private static class BitmapFinalizer {
private long mNativeBitmap;

// Native memory allocated for the duration of the Bitmap,
// if pixel data allocated into native memory, instead of java byte[]
private int mNativeAllocationByteCount;

BitmapFinalizer(long nativeBitmap) {
mNativeBitmap = nativeBitmap;
}

public void setNativeAllocationByteCount(int nativeByteCount) {
if (mNativeAllocationByteCount != 0) {
VMRuntime.getRuntime().registerNativeFree(mNativeAllocationByteCount);
}
mNativeAllocationByteCount = nativeByteCount;
if (mNativeAllocationByteCount != 0) {
VMRuntime.getRuntime().registerNativeAllocation(mNativeAllocationByteCount);
}
}

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

对比Android 7前后的两种实现,最最重要的差别在于使用NativeAllocationRegistry时会判断一下是否需要触发一次java GC,保证GC在最需要内存的时间点前后能执行,而BitmapFinalizer做不到。其次的差别是回收的方式NativeAllocationRegistry使用的是幽灵引用实现的,而BitmapFinalizer依赖ObjectObject的finalize方法😴。