关于android:图片系列6不同版本上-Bitmap-内存分配与回收原理对比

87次阅读

共计 22559 个字符,预计需要花费 57 分钟才能阅读完成。

请点赞关注,你的反对对我意义重大。

🔥 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 硬件位图,能够缩小图片内存调配并进步绘制效率。

源码摘要如下:

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

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

源码摘要如下:

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 治理。


当初,咱们回过头来剖析下 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 治理。


当初,咱们回过头来剖析下 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 像素数据内存的中央。

示意图如下:

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()。

示意图如下:


4. 总结

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


参考资料

  • 治理位图内存 —— Android 官网文档
  • 抖音 Android 性能优化系列:Java OOM 优化之 NativeBitmap 计划 —— 字节跳动技术团队 著
  • 内存优化(上):4GB 内存时代,再谈内存优化 —— 张绍文 著

你的点赞对我意义重大!微信搜寻公众号 [彭旭锐],心愿大家能够一起探讨技术,找到气味相投的敌人,咱们下次见!

正文完
 0