查看原文
其他

不同版本上 Bitmap 内存分配与回收对比

鸿洋
2024-08-30

The following article is from 彭旭锐 Author 彭旭锐

前言


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


源码摘要如下:


Android 7.1 Bitmap.java

http://androidxref.com/7.1.1_r6/xref/frameworks/base/graphics/java/android/graphics/Bitmap.java


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


Android 8.0 Bitmap.java

http://androidxref.com/8.0.0_r4/xref/frameworks/base/graphics/java/android/graphics/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.1Java 堆引用机制
Android 8.0 以后Native 堆 / 硬件引用机制


关于 Finalizer 机制和引用机制的深入分析,见 Finalizer 机制

https://juejin.cn/post/7131027741163388958


程序验证:我们通过一段程序作为佐证,在 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();
    }
}


2Bitmap 创建过程原理分析


这一节,我们来分析 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(…) 为例展开分析。


// 解析资源图片
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

http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp#doDecode


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

http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/jni/android/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 会解析出像素数据的指针。

源码摘要如下:


Android 8.0 SkBitmap.cpp

http://androidxref.com/8.0.0_r4/xref/external/skia/src/core/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

http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/jni/android/graphics/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

http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/jni/android/graphics/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

http://androidxref.com/8.0.0_r4/xref/frameworks/base/libs/hwui/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

http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp#doDecode


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

http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/jni/android/graphics/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

http://androidxref.com/6.0.1_r10/xref/frameworks/base/graphics/java/android/graphics/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 管理。



现在,我们回过头来分析下 doDecode(…) 中间的其它步骤:


步骤 3 - 预分配像素数据内存源码分析:


Android 6.0 这边将步骤 3 和步骤 4 都放在解码器 SkImageDecoder::decode 中,最终通过模板方法 onDecode() 让子类实现,我们以 PNG 的解码器为例。


Android 6.0 SkImageDecoder.cpp

http://androidxref.com/6.0.1_r10/xref/external/skia/src/images/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

http://androidxref.com/6.0.1_r10/xref/external/skia/src/images/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

http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/jni/android/graphics/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

http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp#doFreePixels


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 也成功获得了指向像素数据的指针。


3Bitmap 回收过程原理分析


上一节我们分析了 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

http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/jni/android/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

http://androidxref.com/8.0.0_r4/xref/external/skia/include/core/SkRefCnt.h#fPtr


// 共享指针泛型类,内部维持一个引用计数,并在指针引用计数归零时调用泛型实参的析构函数
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

http://androidxref.com/8.0.0_r4/xref/frameworks/base/libs/hwui/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.javahttp://androidxref.com/8.0.0_r4/xref/libcore/luni/src/main/java/libcore/util/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

http://androidxref.com/8.0.0_r4/xref/libcore/luni/src/main/native/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

http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp#mBitmap


// 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 函数:


Android 6.0 Bitmap.cpp

http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp#doFreePixels


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

http://androidxref.com/6.0.1_r10/xref/frameworks/base/graphics/java/android/graphics/Bitmap.java#nativeRecycle


// 静态内部类 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 官方文档
https://developer.android.google.cn/topic/performance/graphics/manage-memory
抖音 Android 性能优化系列:Java OOM 优化之 NativeBitmap 方案—— 字节跳动技术团队 著
https://juejin.cn/post/7096059314233671694
内存优化(上):4GB内存时代,再谈内存优化—— 张绍文 著

https://time.geekbang.org/column/article/71277




最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读

别滥用FileProvider了,Android中FileProvider的各种场景应用
凡猿修仙传:斩杀ClassNotFoundException when unmarshalling Crash
Android线程锁机制:monitor机制解析


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

继续滑动看下一个
鸿洋
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存