图片系列(6)高低版本 Bitmap 内存分配与回收原理对比

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 图片系列(6)高低版本 Bitmap 内存分配与回收原理对比

前言


Bitmap 是 Android 应用的内存占用大户,是最容易造成 OOM 的场景。为此,Google 也在不断尝试优化 Bitmap 的内存分配和回收策略,涉及:Java 堆、Native 堆、硬件等多种分配方案,未来会不会有新的方案呢?


深入理解 Bitmap 的内存模型是有效开展图片内存优化的基础,在这篇文章里,我将深入 Android 6.0 和 Android 8.0 系统源码,为你总结出不同系统版本上的 Bitmap 运行时内存模型,以及 Bitmap 使用的 Native 内存回收兜底策略。 知其然,知其所以然,开干!


学习路线图:


image.png


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 硬件位图,可以减少图片内存分配并提高绘制效率。


源码摘要如下:

Android 7.1 Bitmap.java

// Native 层 Bitmap 指针
private final long mNativePtr;
// 像素数据
private byte[] mBuffer;
// .9 图信息
private byte[] mNinePatchChunk; // may be null
复制代码

Android 8.0 Bitmap.java


// 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 前的内存分配情况


image.png

GC 后的内存分配情况

image.png

可以看到加载图片后 Native 内存有明显增大,而 GC 后 Native 内存同步下降,符合预期。


1.3 没有必要主动调用 recycle() 吗?


由于 Bitmap 使用了 Finalizer 机制或引用机制来辅助回收,所以当 Java Bitmap 对象被垃圾回收时,也会顺带回收 Native 内存。出于这个原因,网上有观点认为 Bitmap 已经没有必要主动调用 recycle() 方法了,甚至还说是 Google 建议的。真的是这样吗,我们看下 Google 原话是怎么说的:

image.png

不得不说,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 层。


源码摘要如下:


Android 8.0 BitmapFactory.cpp


// 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 管理。

image.png

现在,我们回过头来分析下 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 会解析出像素数据的指针。


源码摘要如下:

Android 8.0 SkBitmap.cpp

// 3. 创建 Native Bitmap 对象并预分配像素数据内存
bool SkBitmap::tryAllocPixels(Allocator* allocator, SkColorTable* ctable) {
    return allocator->allocPixelRef(this, ctable);
}
复制代码


HeapAllocator 内存分配器的定义在 GraphicsJNI.h / Graphics.cpp 中:

Android 8.0 GraphicsJNI.h

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;
};
复制代码

Android 8.0 Graphics.cpp

// 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;
}
复制代码


真正开始分配内存的地方:


Android 8.0 hwui/Bitmap.cpp

// 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 层。


好家伙,创建过程不能说类似,只能说完全一样。直接上源码摘要:


Android 6.0 BitmapFactory.cpp


// 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 对象 源码分析:

Android 6.0 Graphics.cpp

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 构造函数:

Android 6.0 Bitmap.java


// 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 管理。


image.png

现在,我们回过头来分析下 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 6.0 Graphics.cpp


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;
}
复制代码

Android 6.0 Bitmap.cpp


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 泛型类,现在找一下它的定义:


Android 8.0 SkRefCnt.h

// 共享指针泛型类,内部维持一个引用计数,并在指针引用计数归零时调用泛型实参的析构函数
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 会析构,并且像素数据内存也会释放。

Android 8.0 hwui/Bitmap.cpp


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() 返回的函数指针:


graphics/Bitmap.cpp


// 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 像素数据内存的地方。


示意图如下:

image.png

3.3 Android 6.0 回收过程分析


现在我们来分析 Android 6.0 上的 Bitmap 回收过程,相似的步骤我们不会过度分析。

主动调用 recycle() 源码分析:


Java 层调用的 recycle() 方法会走到 Native 层,关联的 JNI 函数:

Android 6.0 Bitmap.cpp


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 内存。源码摘要如下:


Android 6.0 Bitmap.java


// 静态内部类 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()。

示意图如下:


image.png

4. 总结


到这里,Bitmap 的分配和回收过程就分析完了。你会发现在 Android 8.0 以前的版本,Bitmap 的像素数据是存在 Java 堆的,Bitmap 数据放在 Java 堆容易造成 Java OOM,也没有完全利用起来系统 Native 内存。那么,有没有可能让低版本也将 Bitmap 数据存在 Native 层呢?关注我,带你建立核心竞争力,我们下次见。

目录
相关文章
|
1月前
|
算法 JavaScript 前端开发
新生代和老生代内存划分的原理是什么?
【10月更文挑战第29天】新生代和老生代内存划分是JavaScript引擎为了更高效地管理内存、提高垃圾回收效率而采用的一种重要策略,它充分考虑了不同类型对象的生命周期和内存使用特点,通过不同的垃圾回收算法和晋升机制,实现了对内存的有效管理和优化。
|
2月前
|
C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(二)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
2月前
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
2月前
|
存储 C语言 C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(一)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
4月前
|
监控 算法 Java
Java内存管理:垃圾收集器的工作原理与调优实践
在Java的世界里,内存管理是一块神秘的领域。它像是一位默默无闻的守护者,确保程序顺畅运行而不被无用对象所困扰。本文将带你一探究竟,了解垃圾收集器如何在后台无声地工作,以及如何通过调优来提升系统性能。让我们一起走进Java内存管理的迷宫,寻找提高应用性能的秘诀。
|
1月前
|
程序员 开发者
分代回收和手动内存管理相比有何优势
分代回收和手动内存管理相比有何优势
|
2月前
|
算法 Java 程序员
内存回收
【10月更文挑战第9天】
59 5
|
2月前
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
39 2
|
4月前
|
存储 NoSQL 算法
Redis内存回收
Redis 基于内存存储,性能卓越,但单节点内存不宜过大,以免影响持久化或主从同步。可通过配置 `maxmemory` 限制最大内存。内存达到上限时,Redis采用两种策略:内存过期策略和内存淘汰策略。过期策略包括惰性删除和周期删除,后者分为 SLOW 和 FAST 模式。内存淘汰策略有八种,如 LRU、LFU 和随机淘汰等,用于在内存不足时释放空间。官方推荐使用 LFU 算法。
Redis内存回收
|
3月前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。

热门文章

最新文章