携手创造,共同成长!这是我参加「日新计划 8 月更文应战」的第8天,点击查看活动概况

请点赞重视,你的支撑对我含义严重。

Hi,我是小彭。本文已收录到 GitHub AndroidFamily 中。这儿有 Android 进阶成长常识体系,有情投意合的朋友,重视大众号 [彭旭锐] 带你树立中心竞争力。

前言

Bitmap 是 Android 运用的内存占用大户,是最容易形成 OOM 的场景。为此,Google 也在不断测验优化 Bitmap 的内存分配和收回战略,涉及:Java 堆、Native 堆、硬件等多种分配计划,未来会不会有新的计划呢?

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


这篇文章是 Android UI 开发系列图片专题文章第 6 篇,专题文章列表:

  • 1、图片控件:ImageView & Drawable & Bitmap 的联系
  • 2、ImageView 运用详解
  • 3、Bitmap 运用详解
  • 4、Drawable 详解
  • 5、资源图片加载进程
  • 6、Bitmap 内存分配和收回
  • 7、图片解码进程
  • 8、Bitmap 复用机制
  • 9、Bitmap 硬件位图
  • 10、核算图片内存占用
  • 11、常见图片格式
  • 12、图片优化概述
  • 13、图片优化:NativeBitmap
  • 14、图片优化:大图加载
  • 15、图片优化:大图检测
  • 16、图片优化:图片慢呼应排查

学习路线图:

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


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

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

GC 后的内存分配情况

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

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

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

由于 Bitmap 运用了 Finalizer 机制或引证机制来辅佐收回,所以当 Java Bitmap 目标被废物收回时,也会顺带收回 Native 内存。出于这个原因,网上有观念以为 Bitmap 现已没有必要自动调用 recycle() 办法了,乃至还说是 Google 建议的。真的是这样吗,咱们看下 Google 原话是怎样说的:

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

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

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


现在,咱们回过头来剖析下 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 办理。

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


现在,咱们回过头来剖析下 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 像素数据内存的地方。

示意图如下:

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

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

示意图如下:

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


4. 总结

到这儿,Bitmap 的分配和收回进程就剖析完了。你会发现在 Android 8.0 以前的版别,Bitmap 的像素数据是存在 Java 堆的,Bitmap 数据放在 Java 堆容易形成 Java OOM,也没有彻底利用起来体系 Native 内存。那么,有没有可能让低版别也将 Bitmap 数据存在 Native 层呢?重视我,带你树立中心竞争力,咱们下次见。


参考资料

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

你的点赞对我含义严重!微信查找大众号 [彭旭锐],希望大家能够一起讨论技术,找到情投意合的朋友,咱们下次见!

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