图片系列(6)不同版本上 Bitmap 内存分配与回收原理对比
请点赞关注,你的支持对我意义重大。
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<Any>()
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<SkAndroidCodec> 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<jlong>(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<Bitmap> 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<android::Bitmap> 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<Bitmap> (*AllocPixeRef)(size_t allocSize, const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable);
// 3.3 真正开始创建
sk_sp<Bitmap> Bitmap::allocateHeapBitmap(SkBitmap* bitmap, SkColorTable* ctable) {
// 第三个参数是指向 allocateHeapBitmap 的函数指针
return allocateBitmap(bitmap, ctable, &android::allocateHeapBitmap);
}
// 第三个参数为函数指针
static sk_sp<Bitmap> 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<Bitmap> 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<Bitmap>(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<jlong>(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<Bitmap> mBitmap;
...
};
不过,你会发现 hwui/Bitmap.cpp
中并没有 reset() 方法,那 reset() 到底是哪里来的呢?只能从 sk_sp<>
入手了,其实前面的源码中也出现过 sk_sp 泛型类,现在找一下它的定义:
// 共享指针泛型类,内部维持一个引用计数,并在指针引用计数归零时调用泛型实参的析构函数
template <typename T> 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<void*>(static_cast<uintptr_t>(ptr));
FreeFunction nativeFreeFunction = reinterpret_cast<FreeFunction>(static_cast<uintptr_t>(freeFunction));
nativeFreeFunction(nativePtr);
}
这个回收函数就是 Bitmap.java 中的 native 方法 nativeGetNativeFinalizer()
返回的函数指针:
// Java native 方法关联的 JNI 函数
static jlong Bitmap_getNativeFinalizer(JNIEnv*, jobject) {
// 返回 Bitmap_destruct() 的地址
return static_cast<jlong>(reinterpret_cast<uintptr_t>(&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内存时代,再谈内存优化 —— 张绍文 著
你的点赞对我意义重大!微信搜索公众号 [彭旭锐],希望大家可以一起讨论技术,找到志同道合的朋友,我们下次见!
图片系列(6)不同版本上 Bitmap 内存分配与回收原理对比的更多相关文章
- 垃圾回收GC:.Net自己主动内存管理 上(一)内存分配
垃圾回收GC:.Net自己主动内存管理 上(一)内存分配 垃圾回收GC:.Net自己主动内存管理 上(一)内存分配 垃圾回收GC:.Net自己主动内存管理 上(二)内存算法 垃圾回收GC:.Net自己 ...
- Android O Bitmap 内存分配
我们知道,一般认为在Android进程的内存模型中,heap分为两部分,一部分是native heap,一部分是Dalvik heap(实际上也是native heap的一部分). Andro ...
- JVM初探- 内存分配、GC原理与垃圾收集器
JVM初探- 内存分配.GC原理与垃圾收集器 标签 : JVM JVM内存的分配与回收大致可分为如下4个步骤: 何时分配 -> 怎样分配 -> 何时回收 -> 怎样回收. 除了在概念 ...
- Android的内存分配与回收
想写一篇关于android的内存分配和回收文章的想法来源于追查一个魅族手机图片滑动卡顿问题,我们想了很多办法还是没有避免他不停的GC,所以就打算详细的看看内存分配和GC的原理,为什么会不断的GC,GC ...
- linux内存分配与回收
前言 之前在实习时,听了 OOM 的分享之后,就对 Linux 内核内存管理充满兴趣,但是这块知识非常庞大,没有一定积累,不敢写下,担心误人子弟,所以经过一个一段时间的积累,对内核内存有一定了解之后, ...
- Java深入 - Java 内存分配和回收机制
Java的GC机制是自动进行的,和c语言有些区别需要程序员自己保证内存的使用和回收. Java的内存分配和回收也主要在Java的堆上进行的,Java的堆中存储了大量的对象实例,所以Java的堆也叫GC ...
- Java 对象内存分配与回收
JVM内存区域模型: * 程序计数器,内存区域极小,是当前线程的字节码执行行号指示器: * 虚拟机栈.本地方法栈,即平时所说的“栈”,是虚拟机用来执行方法(包括Java.非Java方法)时,使用的临时 ...
- 最简单例子图解JVM内存分配和回收
一.简介 JVM采用分代垃圾回收.在JVM的内存空间中把堆空间分为年老代和年轻代.将大量(据说是90%以上)创建了没多久就会消亡的对象存储在年轻代,而年老代中存放生命周期长久的实例对象.年轻代中又被分 ...
- memcached内存分配及回收初探
对memcached(后面简称mc) 的内存分配及回收机制进行了深度分析和测试,以下是一些学习的心得,和大家共同探讨一下,期望能抛砖引玉 mc简介: mc是由LiveJournal技术团队开发的一套分 ...
随机推荐
- CenterNet和CenterNet2笔记
CenterNet和CenterNet2笔记 CenterNet是基于anchor-free的一阶段检测算法 CenterNet2是CenterNet作者基于两阶段的改进 CenterNet(Obje ...
- 理“ Druid 元数据”之乱
vivo 互联网大数据团队-Zheng Xiaofeng 一.背景 Druid 是一个专为大型数据集上的高性能切片和 OLAP 分析而设计的数据存储系统. 由于Druid 能够同时提供离线和实时数据的 ...
- centos6搭建mysql
目前CentOS6.5及一下版本基本上被官方给放弃更新了,但是考虑到忠实粉丝迟迟不肯放手,所以还留了入口但是非常有限 1.搭建mysql 可参照:https://blog.csdn.net/huang ...
- Centos6添加防火墙端口 以及相关操作命令的使用
用命令 vim /etc/sysconfig/iptables 增加防火墙端口号:(添加你需要的端口号) service iptables start 启动防火墙 service iptables ...
- NB-IoT/LoRa/eMTC和蓝牙/WiFi的关系是互补还是替代?
近年来,相继出现了许多物联网技术.WiFi.蓝牙.NB-IoT.LoRa.eMTC和其他技术为IoT实践提供了一流的技术支持通讯端口.拥有这么多技术,能够互相替代吗?还是能起到互补的作用?为低功耗广域 ...
- 在sqlbolt上学习SQL
在sqlbolt上学习SQL 该网站能够学习sql基础,并且能在网页中直接输入sql语句进行查询. 学习网站原网址https://sqlbolt.com/(!部分指令该网站不支持,且存在一些bug!) ...
- 记一次APP渗透登录验证绕过思路
前言: 起初是抓包时候查看返回状态码不一致,所以觉得是否可以通过修改状态码来达到绕过的目的,但是拦截响应包再替换手速不够,技术大哥就去搜了下,找到了一个方法,可以自动替换响应包内容. 在偏下方一点的地 ...
- 告别单调,Django后台主页改造 - 使用AdminLTE组件
前言 之前我做了个Django的项目,为了让管理后台更加美观,我对Django(应该说是SimpleUI的)默认的Admin后台主页进行改造,具体可以看这篇文章:项目完成 - 基于Django3.x版 ...
- 说什么也要脱单——Python WEB开发:用Tornado框架制作简易【表白墙】网站
先来哔哔两句:(https://jq.qq.com/?_wv=1027&k=QgGWqAVF) 今天我们要用Python做Web开发,做一个简单的[表白墙]网站.众所周知表白墙的功能普遍更多的 ...
- NC16539 [NOIP2013]表达式求值
NC16539 [NOIP2013]表达式求值 题目 题目描述 给定一个只包含加法和乘法的算术表达式,请你编程计算表达式的值. 输入描述 输入仅有一行,为需要你计算的表达式,表达式中只包含数字.加法运 ...