请点赞关注,你的支持对我意义重大。
Hi,我是小彭。本文已收录到 GitHub · AndroidFamily 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。
前言
Bitmap 是 Android 应用的内存占用大户,是最容易造成 OOM 的场景。为此,Google 也在不断尝试优化 Bitmap 的内存分配和回收策略,涉及:Java 堆、Native 堆、硬件等多种分配方案,未来会不会有新的方案呢?
深入理解 Bitmap 的内存模型是有效开展图片内存优化的基础,在这篇文章里,我将深入 Android 6.0 和 Android 8.0 系统源码,为你总结出不同系统版本上的 Bitmap 运行时内存模型,以及 Bitmap 使用的 Native 内存回收兜底策略。 知其然,知其所以然,开干!
学习路线图:
1. 认识 Bitmap 的内存模型
1. 不同版本的 Bitmap 内存分配策略
先说一下 Bitmap 在内存中的组成部分,在任何系统版本中都会存在以下 3 个部分:
- 1、Java Bitmap 对象: 位于 Java 堆,即我们熟悉的
android.graphics.Bitmap.java
; - 2、Native Bitmap 对象: 位于 Native 堆,以
Bitmap.cpp
为代表,除此之外还包括与 Skia 引擎相关的 SkBitmap、SkBitmapInfo 等一系列对象; - 3、图片像素数据: 图片解码后得到的像素数据。
其中,Java Bitmap 对象和 Native Bitmap 对象是分别存储在 Java 堆和 Native 堆的,毋庸置疑。唯一有操作性的是 3、图片像素数据,不同系统版本采用了不同的分配策略,分为 3 个历史时期:
- 时期 1 - Android 3.0 以前: 像素数据存放在 Native 堆(这部分系统版本的市场占有率已经非常低,后文我们不再考虑);
- 时期 2 - Android 8.0 以前: 从 Android 3.0 到 Android 7.1,像素数据存放在 Java 堆;
- 时期 3 - Android 8.0 以后: 从 Android 8.0 开始,像素数据重新存放在 Native 堆。另外还新增了 Hardware Bitmap 硬件位图,可以减少图片内存分配并提高绘制效率。
源码摘要如下:
// Native 层 Bitmap 指针
private final long mNativePtr;
// 像素数据
private byte[] mBuffer;
// .9 图信息
private byte[] mNinePatchChunk; // may be null
// Native 层 Bitmap 指针
private final long mNativePtr;
// 这部分存在 Native 层
// private byte[] mBuffer;
// .9 图信息
private byte[] mNinePatchChunk; // may be null
1.2 不同版本的 Bitmap 内存回收兜底策略
Java Bitmap 对象提供了 recycle()
方法主动释放内存资源。然而, 由于 Native 内存不属于 Java 虚拟机垃圾收集管理的区域,如果不手动调用 recycle() 方法释放资源,即使 Java Bitmap 对象被垃圾回收,位于 Native 层的 Native Bitmap 对象和图片像素数据也不会被回收的。 为了避免 Native 层内存泄漏,Bitmap 内部增加了兜底策略,分为 2 个历史时期:
- 1、Finalizer 机制: 在最初的版本,Bitmap 依赖于 Java Finalizer 机制辅助 Native 内存。Java Finalizer 机制提供了一个在对象被回收之前释放资源的时机,不过 Finalizer 机制是不稳定甚至危险的,所以后续保证 Google 修改了辅助方案;
- 2、引用机制: Android 7.0 开始,开始使用
NativeAllocationRegistry
工具类辅助回收内存。NativeAllocationRegistry 本质上是虚引用的工具类,利用了引用类型感知 Java 对象垃圾回收时机的特性。引用机制相对于 Finalizer 机制更稳定。
用一个表格总结:
分配策略 | 回收兜底策略 | |
---|---|---|
Android 7.0 以前 | Java 堆 | Finalizer 机制 |
Android 7.0 / Android 7.1 | Java 堆 | 引用机制 |
Android 8.0 以后 | Native 堆 / 硬件 | 引用机制 |
关于 Finalizer 机制和引用机制的深入分析,见 Finalizer 机制
程序验证: 我们通过一段程序作为佐证,在 Android 8.0 模拟分配创建 Bitmap 对象后未手动调用 recycle() 方法,观察 Native 内存是否会回收。
示例程序
// 模拟创建 Bitmap 但未主动调用 recycle()
tv.setOnClickListener{
val map = HashSet()
for(index in 0 .. 2){
map.add(BitmapFactory.decodeResource(resources, R.drawable.test))
}
}
GC 前的内存分配情况
GC 后的内存分配情况
可以看到加载图片后 Native 内存有明显增大,而 GC 后 Native 内存同步下降,符合预期。
1.3 没有必要主动调用 recycle() 吗?
由于 Bitmap 使用了 Finalizer 机制或引用机制来辅助回收,所以当 Java Bitmap 对象被垃圾回收时,也会顺带回收 Native 内存。出于这个原因,网上有观点认为 Bitmap 已经没有必要主动调用 recycle() 方法了,甚至还说是 Google 建议的。真的是这样吗,我们看下 Google 原话是怎么说的:
不得不说,Google 这番话确实是有误导性, not need to be called
确实是不需要 / 不必要的意思。抛开这个字眼,我认为 Google 的意思是想说明有兜底策略的存在,如果开发者没有调用 recycle() 方法,也不必担心内存泄漏。如果开发者主动调用 recycle() 方法,则可以获得 advanced
更好的性能 。
再进一步抛开 Google 的观点,站在我们的视角独立思考,你认为需要主动调用 recycle() 方法吗?需要。 Finalizer 机制和引用机制的定位是清晰明确的,它们都是 Bitmap 用来辅助回收内存的兜底策略。虽然从 Finalizer 机制升级到引用机制后稳定性略有提升,或者将来从引用机制升级到某个更优秀的机制,不管怎么升级,兜底策略永远是兜底策略,它永远不会也不能替换主要策略: 在不需要使用资源时立即释放资源。 举个例子,Glide
内部的 Bitmap 缓存池在清除缓存时,会主动调用 recycle() 吗?看源码:
LruBitmapPool.java
// 已简化
private synchronized void trimToSize(long size) {
while (currentSize > size) {
final Bitmap removed = strategy.removeLast();
currentSize -= strategy.getSize(removed);
// 主动调用 recycle()
removed.recycle();
}
}
2. Bitmap 创建过程原理分析
这一节,我们来分析 Bitmap 的创建过程。由于 Android 8.0 前后采用了不同的内存分配方案,而 Android 7.0 前后采用了不同的内存回收兜底方案,综合考虑我选择从 Android 6.0 和 Android 8.0 展开分析:
2.1 BitmapFactory 工厂类
Bitmap 的构造方法是非公开的,创建 Bitmap 只能通过 BitmapFactory 或 Bitmap 的静态方法创建,即使 ImageDecoder 内部也是通过 BitmapFactory 创建 Bitmap 的。
BitmapFactory 工厂类提供了从不同数据源加载图片的能力,例如资源图片、本地图片、内存中的 byte 数组等。不管怎么样,最终还是通过 native 方法来创建 Bitmap 对象,下面我们以 nativeDecodeStream(…)
为例展开分析。
BitmapFactory.java
// 解析资源图片
public static Bitmap decodeResource(Resources res, int id)
// 解析本地图片
public static Bitmap decodeFile(String pathName)
// 解析文件描述符
public static Bitmap decodeFileDescriptor(FileDescriptor fd)
// 解析 byte 数组
public static Bitmap decodeByteArray(byte[] data, int offset, int length)
// 解析输入流
public static Bitmap decodeStream(InputStream is)
// 最终通过 Native 层创建 Bitmap 对象
private static native Bitmap nativeDecodeStream(...);
private static native Bitmap nativeDecodeFileDescriptor(...);
private static native Bitmap nativeDecodeAsset(...);
private static native Bitmap nativeDecodeByteArray(...);
2.2 Android 8.0 创建过程分析
Android 8.0 之前的版本相对过时了,我决定把精力向更时新的版本倾斜,所以我们先分析 Android 8.0 中的创建过程。Java 层调用的 native 方法最终会走到 doDecode(…)
函数中,内部的逻辑非常复杂,我将整个过程概括为 5 个步骤:
- 步骤 1 - 创建解码器: 创建一个面向输入流的解码器;
- 步骤 2 - 创建内存分配器: 创建像素数据的内存分配器,默认使用 Native Heap 内存分配器(
HeapAllocator
),如果使用了inBitmap
复用会采用其他分配器; - 步骤 3 - 预分配像素数据内存: 使用内存分配器预分配内存,并创建 Native Bitmap 对象;
- 步骤 4 - 解码: 使用解码器解码,并写入到预分配内存;
- 步骤 5 - 返回 Java Bitmap 对象: 创建 Java Bitmap 对象,并包装了指向 Native Bitmap 的指针,返回到 Java 层。
源码摘要如下:
// Java native 方法关联的 JNI 函数
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage, jobject padding, jobject options) {
// 已简化
return doDecode(env, bufferedStream.release(), padding, options);
}
// 核心方法
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
// 省略 BitmapFactory.Options 参数读取
// 1. 创建解码器
NinePatchPeeker peeker;
std::unique_ptr codec(SkAndroidCodec::NewFromStream(streamDeleter.release(), &peeker));
// 2. 创建内存分配器
// HeapAllocator:在 Native Heap 分配内存
HeapAllocator defaultAllocator;
SkBitmap::Allocator* decodeAllocator = &defaultAllocator;
SkBitmap decodingBitmap;
// 图片参数信息(在下文源码中会用到)
const SkImageInfo bitmapInfo = SkImageInfo::Make(size.width(), size.height(), decodeColorType, alphaType, decodeColorSpace);
// 3. 预分配像素数据内存
// tryAllocPixels():创建 Native Bitmap 对象并预分配像素数据内存
if (!decodingBitmap.setInfo(bitmapInfo) || !decodingBitmap.tryAllocPixels(decodeAllocator, colorTable.get())) {
// 异常 1:Java OOM
// 异常 2:Native OOM
// 异常 3:复用已调用 recycle() 的 Bitmap
return nullptr;
}
// 4. 解码
// getAndroidPixel():解码并写入像素数据内存地址
// getPixels():像素数据内存地址
// rowBytes():像素数据大小
SkCodec::Result result = codec->getAndroidPixels(decodeInfo, decodingBitmap.getPixels(), decodingBitmap.rowBytes(), &codecOptions);
switch (result) {
case SkCodec::kSuccess:
case SkCodec::kIncompleteInput:
break;
default:
return nullObjectReturn("codec->getAndroidPixels() failed.");
}
// 省略 .9 图逻辑
// 省略 sample 缩放逻辑
// 省略 inBitmap 复用逻辑
// 省略 Hardware 硬件位图逻辑
// 5. 创建 Java Bitmap 对象
// defaultAllocator.getStorageObjAndReset():获取 Native 层 Bitmap 对象
return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}
中间几个步骤的源码先放到一边,我们先把注意力放到决定函数返回值最后一个步骤上。
步骤 5 - 返回 Java Bitmap 对象 源码分析:
Android 8.0 graphics/Bitmap.cpp
jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets, int density) {
...
// 5.1 创建 BitmapWrapper 包装类
BitmapWrapper* bitmapWrapper = new BitmapWrapper(bitmap);
// 5.2 调用 Java 层 Bitmap 构造函数
jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
reinterpret_cast(bitmapWrapper), bitmap->width(), bitmap->height(), density,
isMutable, isPremultiplied, ninePatchChunk, ninePatchInsets);
return obj;
}
// BitmapWrapper 是对 Native Bitmap 的包装类,本质还是 Native Bitmap
class BitmapWrapper {
public:
BitmapWrapper(Bitmap* bitmap) : mBitmap(bitmap) { }
...
private:
// Native Bitmap 指针
sk_sp mBitmap;
...
};
Java 层 Bitmap 构造函数:
Android 8.0 Bitmap.java
// Native Bitmap 指针
private final long mNativePtr;
// .9 图信息
private byte[] mNinePatchChunk; // may be null
// 从 JNI 层调用
Bitmap(long nativeBitmap, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
...
// 宽度
mWidth = width;
// 高度
mHeight = height;
// .9 图信息
mNinePatchChunk = ninePatchChunk;
// Native Bitmap 指针
mNativePtr = nativeBitmap;
...
}
可以看到,第 5 步是调用 Java Bitmap 的构造函数创建 Java Bitmap 对象,并传递一个 Native Bitmap 指针 nativeBitmap
。 至此,Bitmap 对象创建完毕,Java Bitmap 持有一个指向 Native Bitmap 的指针,像素数据由 Native 管理。
现在,我们回过头来分析下 doDecode(…)
中间的其它步骤:
步骤 3 - 预分配像素数据内存源码分析:
HeapAllocator
是默认的分配器,用于在 Native Heap 上分配像素数据内存。内部经过一系列跳转后,最终核心的源码分为 4 步:
- 3.3.1 获取图片参数信息(在上文提到过图片参数信息);
- 3.3.2 计算像素数据内存大小;
- 3.3.3 创建 Native Bitmap 对象并分配像素数据内存空间(使用库函数 calloc 分配了一块连续内存);
- 3.3.4 关联 SkBitmap 与 Native Bitmap,SkBitmap 会解析出像素数据的指针。
源码摘要如下:
// 3. 创建 Native Bitmap 对象并预分配像素数据内存
bool SkBitmap::tryAllocPixels(Allocator* allocator, SkColorTable* ctable) {
return allocator->allocPixelRef(this, ctable);
}
HeapAllocator 内存分配器的定义在 GraphicsJNI.h / Graphics.cpp 中:
class HeapAllocator : public SkBRDAllocator {
public:
// 3.1 分配内存函数原型
virtual bool allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) override;
// 返回 Native Bitmap 的指针
android::Bitmap* getStorageObjAndReset() {
return mStorage.release();
};
SkCodec::ZeroInitialized zeroInit() const override { return SkCodec::kYes_ZeroInitialized; }
private:
// Native Bitmap 的指针
sk_sp mStorage;
};
// 3.2 分配内存函数实现
// 创建 Native Bitmap 对象,并将指针记录到 HeapAllocator#mStorage 字段中
bool HeapAllocator::allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) {
// 3.4 记录 Native Bitmap 的指针
mStorage = android::Bitmap::allocateHeapBitmap(bitmap, ctable);
return !!mStorage;
}
真正开始分配内存的地方:
// AllocPixeRef 为函数指针,类似于 Kotlin 的高阶函数
typedef sk_sp (*AllocPixeRef)(size_t allocSize, const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable);
// 3.3 真正开始创建
sk_sp Bitmap::allocateHeapBitmap(SkBitmap* bitmap, SkColorTable* ctable) {
// 第三个参数是指向 allocateHeapBitmap 的函数指针
return allocateBitmap(bitmap, ctable, &android::allocateHeapBitmap);
}
// 第三个参数为函数指针
static sk_sp allocateBitmap(SkBitmap* bitmap, SkColorTable* ctable, AllocPixeRef alloc) {
// info:图片参数
// size:像素数据内存大小
// rowBytes:一行占用的内存大小
// 3.3.1 获取图片参数信息(SkImageInfo 在上文提到了)
const SkImageInfo& info = bitmap->info();
size_t size;
const size_t rowBytes = bitmap->rowBytes();
// 3.3.2 计算像素数据内存大小,并将结果赋值到 size 变量上
if (!computeAllocationSize(rowBytes, bitmap->height(), &size)) {
return nullptr;
}
// 3.3.3 创建 Native Bitmap 对象并分配像素数据内存空间
auto wrapper = alloc(size, info, rowBytes, ctable);
// 3.3.4 关联 SkBitmap 与 Native Bitmap
wrapper->getSkBitmap(bitmap);
bitmap->lockPixels();
return wrapper;
}
// 函数指针指向的函数
// 3.3.2 创建 Native Bitmap 对象并预分配像素数据内存
static sk_sp allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable) {
// 3.3.2.1 使用库函数 calloc 分配 size*1 的连续空间
void* addr = calloc(size, 1);
// 3.3.2.2 创建 Native Bitmap 对象
return sk_sp(new Bitmap(addr, size, info, rowBytes, ctable));
}
// 3.3.2.2 Native Bitmap 构造函数
Bitmap::Bitmap(void* address, size_t size, const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable)
: SkPixelRef(info)
, mPixelStorageType(PixelStorageType::Heap) {
// 指向像素数据的内存指针(在回收过程源码中会用到)
mPixelStorage.heap.address = address;
// 像素数据大小
mPixelStorage.heap.size = size;
reconfigure(info, rowBytes, ctable);
}
// 3.3.3 关联 SkBitmap 与 Native Bitmap
void Bitmap::getSkBitmap(SkBitmap* outBitmap) {
...
// 让 SkBitmap 持有 Native Bitmap 的指针,SkBitmap 会解析出像素数据的指针
outBitmap->setPixelRef(this);
}
至此,Native Bitmap 和像素数据内存空间都准备好了,SkBitmap 也成功获得了指向 Native 堆像素数据的指针。 下一步就由 Skia 引擎的解码器对输入流解码并写入这块内存中,Skia 引擎我们下次再讨论,我们今天主要讲 Bitmap 的核心流程。
2.3 Android 6.0 创建过程分析
现在我们来分析 Android 6.0 上的 Bitmap 创建过程,理解 Android 8.0 的分配过程后就驾轻就熟了。Java 层调用的 native 方法最终也会走到 doDecode(…)
函数中,内部的逻辑非常复杂,我将整个过程概括为 5 个步骤:
- 步骤 1 - 创建解码器: 创建一个面向输入流的解码器;
- 步骤 2 - 创建内存分配器: 创建像素数据的内存分配器,默认使用 Java Heap 内存分配器(
JavaPixelAllocator
),如果使用了inBitmap
复用会采用其他分配器; - 步骤 3 - 预分配像素数据内存: 预分配像素数据内存空间,并创建 Native Bitmap 对象;
- 步骤 4 - 解码: 使用解码器解码,并写入到预分配内存;
- 步骤 5 - 返回 Java Bitmap 对象: 创建 Java Bitmap 对象,并包装了指向 Native Bitmap 的指针,返回到 Java 层。
好家伙,创建过程不能说类似,只能说完全一样。直接上源码摘要:
// Java native 方法关联的 JNI 函数
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage, jobject padding, jobject options) {
// 已简化
return doDecode(env, bufferedStream.release(), padding, options);
}
// 核心方法
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
// 省略 BitmapFactory.Options 参数读取
// 1. 创建解码器
SkImageDecoder* decoder = SkImageDecoder::Factory(stream);
NinePatchPeeker peeker(decoder);
decoder->setPeeker(&peeker);
// 2. 创建内存分配器
JavaPixelAllocator javaAllocator(env);
decoder->setAllocator(javaAllocator);
// 3. 预分配像素数据内存
// 4. 解码
// decode():创建 Native Bitmap 对象、预分配像素数据内存、解码
SkBitmap decodingBitmap;
if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode) != SkImageDecoder::kSuccess) {
return nullObjectReturn("decoder->decode returned false");
}
// 省略 .9 图逻辑
// 省略 sample 缩放逻辑
// 省略 inBitmap 复用逻辑
// 5. 创建 Java Bitmap 对象
// javaAllocator.getStorageObjAndReset():获取 Native 层 Bitmap 对象
return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}
中间几个步骤的源码先放到一边,我们同样先把注意力放到决定函数返回值最后一个步骤上。
步骤 5 - 返回 Java Bitmap 对象 源码分析:
jobject GraphicsJNI::createBitmap(JNIEnv* env, android::Bitmap* bitmap,
int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
int density) {
// 调用 Java 层 Bitmap 构造函数
jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
reinterpret_cast(bitmap), bitmap->javaByteArray(),
bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied,
ninePatchChunk, ninePatchInsets);
return obj;
}
Java 层 Bitmap 构造函数:
// Native Bitmap 指针
private final long mNativePtr;
// .9 图信息
private byte[] mNinePatchChunk; // may be null
// 从 JNI 层调用
Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
...
// 宽度
mWidth = width;
// 高度
mHeight = height;
// .9 图信息
mNinePatchChunk = ninePatchChunk;
// Native Bitmap 指针
mNativePtr = nativeBitmap;
}
可以看到,第 5 步是调用 Java Bitmap 的构造函数创建 Java Bitmap 对象,并传递一个 Native Bitmap 指针 nativeBitmap
和一个 byte[] 对象 buffer
。 至此,Bitmap 对象创建完毕,Java Bitmap 持有一个指向 Native Bitmap 的指针,像素数据由 Java 管理。
现在,我们回过头来分析下 doDecode(…)
中间的其它步骤:
步骤 3 - 预分配像素数据内存源码分析:
Android 6.0 这边将步骤 3 和步骤 4 都放在解码器 SkImageDecoder::decode
中,最终通过模板方法 onDecode()
让子类实现,我们以 PNG 的解码器为例。
Android 6.0 SkImageDecoder.cpp
SkImageDecoder::Result SkImageDecoder::decode(SkStream* stream, SkBitmap* bm, SkColorType pref, Mode mode) {
SkBitmap tmp;
// onDecode 由子类实现
const Result result = this->onDecode(stream, &tmp, mode);
if (kFailure != result) {
bm->swap(tmp);
}
return result;
}
Android 6.0 SkImageDecoder_libpng.cpp
SkImageDecoder::Result SkPNGImageDecoder::onDecode(SkStream* sk_stream, SkBitmap* decodedBitmap, Mode mode) {
...
// 3. 预分配像素数据内存
if (!this->allocPixelRef(decodedBitmap, kIndex_8_SkColorType == colorType ? colorTable : NULL)) {
return kFailure;
}
// 4. 解码
...
}
相似的流程我们就不要过度分析了,反正也是通过 JavaPixelAllocator 分配内存的。JavaPixelAllocator 最终调用 allocateJavaPixelRef() 创建 Native Bitmap 对象:
android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap, SkColorTable* ctable) {
// info:图片参数
// size:像素数据内存大小
// rowBytes:一行占用的内存大小
// 3.1 获取图片参数信息(SkImageInfo 在上文提到了)
const SkImageInfo& info = bitmap->info();
size_t size;
// 3.2 计算像素数据内存大小,并将结果赋值到 size 变量上
if (!computeAllocationSize(*bitmap, &size)) {
return NULL;
}
const size_t rowBytes = bitmap->rowBytes();
// 3.3 创建 Java byte 数组对象,数组大小为 size
jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime, gVMRuntime_newNonMovableArray, gByte_class, size);
// 3.4 获取 byte 数组
jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj);
// 3.5 创建 Native Bitmap 对象
android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr, info, rowBytes, ctable);
// 3.6 关联 SkBitmap 与 Native Bitmap
wrapper->getSkBitmap(bitmap);
bitmap->lockPixels();
return wrapper;
}
Bitmap::Bitmap(JNIEnv* env, jbyteArray storageObj, void* address,
const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable)
: mPixelStorageType(PixelStorageType::Java) {
env->GetJavaVM(&mPixelStorage.java.jvm);
// 像素数据指针(在回收过程源码中会用到)
// 由于 strongObj 是局部变量,不能跨线程和跨方法使用,所以这里升级为弱全局引用
mPixelStorage.java.jweakRef = env->NewWeakGlobalRef(storageObj);
mPixelStorage.java.jstrongRef = nullptr;
mPixelRef.reset(new WrappedPixelRef(this, address, info, rowBytes, ctable));
mPixelRef->unref();
}
与 Android 8.0 对比区别不大,关键区别是像素数据内存的方式不一样:
- Android 8.0 前:调用 Java 方法创建 Java byte 数组,在 Java 堆分配内存;
- Android 8.0 后: 调用库函数 calloc 在 Native 堆分配内存。
至此,Native Bitmap 和像素数据内存空间都准备好了,SkBitmap 也成功获得了指向像素数据的指针。
3. Bitmap 回收过程原理分析
上一节我们分析了 Bitmap 的创建过程,有创建就会有释放,这一节我们来分析 Bitmap 的内收过程,我们继续从 Android 6.0 和 Android 8.0 展开分析:
3.1 recycle() 回收方法
Java Bitmap 对象提供了 recycle()
方法主动释放内存资源,内部会调用 native 方法来释放 Native 内存。调用 recycle() 后的 Bitmap 对象会被标记为 “死亡” 状态,内部大部分方法都不在允许使用。因为不管像素数据是存在 Java 堆还是 Native 堆,Native Bitmap 这部分内存永远是在 Native 内存的,所以 native 方法这一步少不了。
Bitmap.java
// 回收标记位
private boolean mRecycled;
public void recycle() {
if (!mRecycled) {
// 括号内这部分在不同版本略有区别,但差别不大
// 调用 native 方法释放内存
nativeRecycle(mNativePtr);
mRecycled = true;
}
}
public final boolean isRecycled() {
return mRecycled;
}
public final int getWidth() {
if (mRecycled) {
Log.w(TAG, "Called getWidth() on a recycle()'d bitmap! This is undefined behavior!");
}
return mWidth;
}
3.2 Android 8.0 回收过程分析
同理,我们先分析 Android 8.0 的回收过程。
主动调用 recycle() 源码分析: Java 层调用的 recycle() 方法最终会走到 Native 层 Bitmap_recycle(…)
函数中,源码摘要如下:
Android 8.0 Bitmap.java
public void recycle() {
if (!mRecycled) {
nativeRecycle(mNativePtr);
mNinePatchChunk = null;
mRecycled = true;
}
}
// 使用 Native Bitmap 指针来回收
private static native void nativeRecycle(long nativeBitmap);
关联的 JNI 函数:
Android 8.0 graphics/Bitmap.cpp
// Java native 方法关联的 JNI 函数
static jboolean Bitmap_recycle(JNIEnv* env, jobject, jlong bitmapHandle) {
// 根据分配过程的分析,我们知道 bitmapHandle 是 BitmapWrapper 类型
LocalScopedBitmap bitmap(bitmapHandle);
bitmap->freePixels();
return JNI_TRUE;
}
class BitmapWrapper {
public:
BitmapWrapper(Bitmap* bitmap): mBitmap(bitmap) { }
void freePixels() {
...
mBitmap.reset();
}
...
private:
// Native Bitmap 指针
sk_sp mBitmap;
...
};
不过,你会发现 hwui/Bitmap.cpp
中并没有 reset() 方法,那 reset() 到底是哪里来的呢?只能从 sk_sp<>
入手了,其实前面的源码中也出现过 sk_sp 泛型类,现在找一下它的定义:
// 共享指针泛型类,内部维持一个引用计数,并在指针引用计数归零时调用泛型实参的析构函数
template class sk_sp {
public:
void reset(T* ptr = nullptr) {
T* oldPtr = fPtr;
fPtr = ptr;
oldPtr.unref();
}
private:
T* fPtr;
};
原来 sk_sp<>
是 Skia 内部定义的一个泛型类,能够实现共享指针在引用计数归零时自动调用对象的析构函数。 这说明 reset()
最终会走到 hwui/Bitmap.cpp 的析构函数,并在 PixelStorageType::Heap 分支中通过 free()
释放先前 calloc()
动态分配的内存。 Nice,闭环了。不仅 Native Bitmap 会析构,并且像素数据内存也会释放。
Bitmap::~Bitmap() {
switch (mPixelStorageType) {
case PixelStorageType::External:
// 外部方式(在源码中未查到找相关调用)
mPixelStorage.external.freeFunc(mPixelStorage.external.address, mPixelStorage.external.context);
break;
case PixelStorageType::Ashmem:
// mmap ashmem 内存(用于跨进程传递 Bitmap,例如 Notification)
munmap(mPixelStorage.ashmem.address, mPixelStorage.ashmem.size);
close(mPixelStorage.ashmem.fd);
break;
case PixelStorageType::Heap:
// Native 堆内存
// mPixelStorage.heap.address 在上文提到了
free(mPixelStorage.heap.address);
break;
case PixelStorageType::Hardware:
// 硬件位图
auto buffer = mPixelStorage.hardware.buffer;
buffer->decStrong(buffer);
mPixelStorage.hardware.buffer = nullptr;
break;
}
android::uirenderer::renderthread::RenderProxy::onBitmapDestroyed(getStableID());
}
引用机制兜底源码分析: 在 Bitmap 构造器中,会创建 NativeAllocationRegistry 工具类来辅助回收 Native 内存,它背后利用了引用类型感知垃圾回收时机的机制,从而实现 Java Bitmap 对象被垃圾回收时确保回收底层 Native 内存。源码摘要如下:
Android 8.0 Bitmap.java
// 从 JNI 层调用
Bitmap(long nativeBitmap, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
...
// NativeBitmap 指针
mNativePtr = nativeBitmap;
// 创建 NativeAllocationRegistry 工具
// 1. nativeGetNativeFinalizer(): Native 层回收函数指针
// 2. nativeSize:Native 内存占用大小
// 3. this:Java Bitmap
// 4. nativeBitmap:Native 对象指针
long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
NativeAllocationRegistry registry = new NativeAllocationRegistry(Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
registry.registerNativeAllocation(this, nativeBitmap);
}
public final int getAllocationByteCount() {
return nativeGetAllocationByteCount(mNativePtr);
}
// 获取 Native 层回收函数的函数指针
private static native long nativeGetNativeFinalizer();
// 获取 Native 内存占用
private static native int nativeGetAllocationByteCount(long nativeBitmap);
Android 8.0 NativeAllocationRegistry.java
public class NativeAllocationRegistry {
private final ClassLoader classLoader;
private final long freeFunction;
private final long size;
public NativeAllocationRegistry(ClassLoader classLoader, long freeFunction, long size) {
this.classLoader = classLoader;
this.freeFunction = freeFunction;
this.size = size;
}
public Runnable registerNativeAllocation(Object referent, long nativePtr) {
// 1. 向虚拟机声明 Native 内存占用
registerNativeAllocation(this.size);
// 2. 创建 Cleaner 工具类(本质上是封装了虚引用与引用队列)
Cleaner cleaner = Cleaner.create(referent, new CleanerThunk(nativePtr));
return new CleanerRunner(cleaner);
}
// 3. Cleaner 机制的回收函数
private class CleanerThunk implements Runnable {
private long nativePtr;
public CleanerThunk(long nativePtr) {
this.nativePtr = nativePtr;
}
public void run() {
// 4. 调用 Native 函数
applyFreeFunction(freeFunction, nativePtr);
// 5. 向虚拟机声明 Native 内存释放
registerNativeFree(size);
}
}
private static void registerNativeAllocation(long size) {
VMRuntime.getRuntime().registerNativeAllocation((int)Math.min(size, Integer.MAX_VALUE));
}
private static void registerNativeFree(long size) {
VMRuntime.getRuntime().registerNativeFree((int)Math.min(size, Integer.MAX_VALUE));
}
public static native void applyFreeFunction(long freeFunction, long nativePtr);
}
关联的 JNI 函数:
Android 8.0 libcore_util_NativeAllocationRegistry.cpp
// FreeFunction 是函数指针
typedef void (*FreeFunction)(void*);
static void NativeAllocationRegistry_applyFreeFunction(JNIEnv*, jclass, jlong freeFunction, jlong ptr) {
// 执行函数指针指向的回收函数
void* nativePtr = reinterpret_cast(static_cast(ptr));
FreeFunction nativeFreeFunction = reinterpret_cast(static_cast(freeFunction));
nativeFreeFunction(nativePtr);
}
这个回收函数就是 Bitmap.java 中的 native 方法 nativeGetNativeFinalizer()
返回的函数指针:
// Java native 方法关联的 JNI 函数
static jlong Bitmap_getNativeFinalizer(JNIEnv*, jobject) {
// 返回 Bitmap_destruct() 的地址
return static_cast(reinterpret_cast(&Bitmap_destruct));
}
static void Bitmap_destruct(BitmapWrapper* bitmap) {
// 执行 delete 释放 Native Bitmap,最终会执行 Native Bitmap 的析构函数
delete bitmap;
}
可以看到,Bitmap 就是拿到一个 Native 层的回收函数然后注册到 NativeAllocationRegistry 工具里,NativeAllocationRegistry 内部再通过 Cleaner 机制包装了一个回收函数 CleanerThunk
。 最终,当 Java Bitmap 被垃圾回收时,就会在 Native 层 delete
Native Bitmap 对象,随即执行析构函数,也就衔接到最后 free
像素数据内存的地方。
示意图如下:
3.3 Android 6.0 回收过程分析
现在我们来分析 Android 6.0 上的 Bitmap 回收过程,相似的步骤我们不会过度分析。
主动调用 recycle() 源码分析:
Java 层调用的 recycle() 方法会走到 Native 层,关联的 JNI 函数:
static jboolean Bitmap_recycle(JNIEnv* env, jobject, jlong bitmapHandle) {
// 根据分配过程的分析,我们知道 bitmapHandle 是 Bitmap 类型
LocalScopedBitmap bitmap(bitmapHandle);
bitmap->freePixels();
return JNI_TRUE;
}
void Bitmap::freePixels() {
doFreePixels();
mPixelStorageType = PixelStorageType::Invalid;
}
void Bitmap::doFreePixels() {
switch (mPixelStorageType) {
case PixelStorageType::Invalid:
// already free'd, nothing to do
break;
case PixelStorageType::External:
// 外部方式(在源码中未查到找相关调用)
mPixelStorage.external.freeFunc(mPixelStorage.external.address, mPixelStorage.external.context);
break;
case PixelStorageType::Ashmem:
// mmap ashmem 内存(用于跨进程传递 Bitmap,例如 Notification)
munmap(mPixelStorage.ashmem.address, mPixelStorage.ashmem.size);
close(mPixelStorage.ashmem.fd);
break;
case PixelStorageType::Java:
// Java 堆内存
// mPixelStorage.java.jweakRef 在上文提到了
JNIEnv* env = jniEnv();
// 释放弱全局引用
env->DeleteWeakGlobalRef(mPixelStorage.java.jweakRef);
break;
}
if (android::uirenderer::Caches::hasInstance()) {
android::uirenderer::Caches::getInstance().textureCache.releaseTexture( mPixelRef->getStableID());
}
}
可以看到,调用 recyele() 最终只是释放了像素数据数组的弱全局引用。
Finalizer 机制兜底源码分析:
在 Bitmap 的 finalize() 方法中,会调用 Native 方法辅助回收 Native 内存。源码摘要如下:
// 静态内部类 BitmapFinalizer:
public void finalize() {
setNativeAllocationByteCount(0);
nativeDestructor(mNativeBitmap);
mNativeBitmap = 0;
}
关联的 JNI 函数:
static void Bitmap_destructor(JNIEnv* env, jobject, jlong bitmapHandle) {
LocalScopedBitmap bitmap(bitmapHandle);
bitmap->detachFromJava();
}
void Bitmap::detachFromJava() {
...
// 释放当前对象
delete this;
}
// 析构函数也会调用 doFreePixels()
Bitmap::~Bitmap() {
doFreePixels();
}
可以看到,finalize() 最终会调用 delete
释放 Native Bitmap。如果没有主动调用 recycle(),在 Native Bitmap 的析构函数中也会走到 doFreePixels()。
示意图如下:
4. 总结
到这里,Bitmap 的分配和回收过程就分析完了。你会发现在 Android 8.0 以前的版本,Bitmap 的像素数据是存在 Java 堆的,Bitmap 数据放在 Java 堆容易造成 Java OOM,也没有完全利用起来系统 Native 内存。那么,有没有可能让低版本也将 Bitmap 数据存在 Native 层呢?关注我,带你建立核心竞争力,我们下次见。
参考资料
- 管理位图内存 —— Android 官方文档
- 抖音 Android 性能优化系列:Java OOM 优化之 NativeBitmap 方案 —— 字节跳动技术团队 著
- 内存优化(上):4GB内存时代,再谈内存优化 —— 张绍文 著
你的点赞对我意义重大!微信搜索公众号 [彭旭锐],希望大家可以一起讨论技术,找到志同道合的朋友,我们下次见!