目录

NativeBitmap 实现

前言

最近一直在治理 OOM 问题,OOM 问题分为多种,其中有一种的原因是 java heap 空间不足

这种 OOM 多发生于低版本手机,或者是存在严重的内存泄漏的高版本手机

现在很多 APP minSdk 都升级到了 21, 这时候低版本 Bitmap 一直分配在 java heap 中,是内存大户,一张大图占用10M级别的内存很正常。而一些低版本的 java heap 只有 128/256M,很容易就 OOM

那有没有办法减少这种 OOM 的发生呢?《抖音 NativeBitmap》 就是一种解决方案

通过 hook 一些函数,让低版本的 Bitmap 分配到 native heap 空间,从而降低 java heap 的压力。Android 8.0 以后系统默认就是将 Bitmap 内存部分分配到 native heap,所以我们不需要关心高版本的问题

当然,字节还有一种大对象直接绕过 java heap 统计的方案 https://juejin.cn/post/7052574440734851085,一样可以拯救 OOM,不过还没有仔细研究,也不是这篇文章要讨论的核心

NativeBitmap 实现

前面的文章只给了大致的思路,并没有开源,但是搜索 github,会发现一个作者看起来是实现了 nativebitmap(https://github.com/shaomaicheng/nativebitmap),实则不然

核心思路

既然是将 Bitmap 的 buffer 分配从 java 移动到 native,那一定少不了 hook

Bitmap 分配和释放的流程的源码分析看抖音的文章即可

这里说两点:

  1. Bitmap 分配在 java heap 中,会调用 allocateJavaPixelRef,最终使用了 newNonMovableArray(申请 java byte[]),然后使用 addressOf 方法获取数组首地址,然后传递赋值

    这里就可以想到,hook newNonMovableArray,然后创建一个假数组,假定先不考虑其他问题的话,数组中直接存储一个指针,指向 native byte[],这个问题就解决了

  2. Bitmap 释放最终都会走到 DeleteWeakGlobalRef

    先不考虑其他问题, hook DeleteWeakGlobalRef,如果我们能够找到之前分配的 native heap,把 native 内存释放掉,那一切都完美了!

核心就这两点,但是围绕这两个问题,会有许多问题:

  1. hook newNonMovableArray 这种内部方法,怎么 hook 呢?

  2. newNonMovableArray 是 java byte[] 创建的基础方法,很多情况都会调用,怎么能知道这个 byte[] 是 Bitmap 申请的呢?如果不是 Bitmap 申请的,我们肯定不能做任何处理,不然正常的 byte[] 数组被我们篡改,程序运行肯定会出现各种诡异的问题

  3. newNonMovableArray 创建的假数组的格式是怎样的?

  4. hook newNonMovableArray 构造假的数组,在 DeleteWeakGlobalRef 并不一定还可以引用到,DeleteWeakGlobalRef 中的 jweak 类型也不是唯一的 byte[],那我们怎么找到 native byte[] 那边内存地址呢?如果不手动 free,那就是 native 层的内存泄漏

  5. 原文中最后提到的 registerNativeAllocation 和 registerNativeFree 方法调用

问题1

使用 inline hook 即可,github 的私人开源版本也是这么实现的

问题2

github 的私人开源版本并没有处理,直接 hook 了所有的 newNonMovableArray,虽然也有 unhook 的时机,但是感觉不太对,而且用一个 map 存储了一些映射关系,这些过程如果存在并发都会有问题。

其实这里,我们可以直接 hook allocateJavaPixelRef 方法,当执行到这个方法时标记为 bitmap 内存申请,然后在 newNonMovableArray 中做逻辑判断是否要 hook byte[] 的创建。

但是,显然这里又有一个问题,任何线程任何时刻都有可能调用 allocateJavaPixelRef,所以我们需要一个 thread local 变量来存储,当前线程当前时刻是否是在为 Bitmap 申请一个 byte[],绕过了并发的问题。

allocateJavaPixelRef 函数中有很多 skia 引擎的结构,但是我们可以完全不用管它们,直接 void* 就好了。github 版本似乎引入了众多头文件,感觉没必要。

问题3 和 问题4

这部分原文章讲解的很清晰。因为 java 层 byte[] 本身就有自己的结构:

kclass_ uint32_t monitor_ size data
4 byte 4 byte 4 byte size byte

原本 data 区域是 java byte[],addressof 函数返回的是 data 区域的首地址。现在,我们可以 hook newNonMovableArray 和 addressof 函数,让它返回我们自己手动从 native 申请的内存地址,那就完成了 java byte[] 的替换。

举个例子,假设此时 Bitmap 打算申请 1024 字节,我们 hook 了 newNonMovableArray 和 addressOf 和 DeleteWeakGlobalRef

// 几个函数原型
jbyteArray newNonMovableArray(JNIEnv *env, jobject obj, jclass javaElementClass, jint )

jlong addressOf(JNIEnv *env, jobject obj, jbyteArray javaArray)

void DeleteWeakGlobalRef(JNIEnv *env, jweak obj)

我们显然不能让 newNonMovableArray 这个方法按原始逻辑执行,我们需要自己创建一个小的 java byte[] 原文叫做 fakeArray,这个 fakeArray 的 size 参数直接修改成 真实需要的byte[] 的大小,真实需要的 byte[] 我们直接使用 malloc 在 native 分配,data 区域存储 native byte[] 指针就好

于是,这个 fakeArray 目前的样子是:

kclass_ uint32_t monitor_ size native byte[] pointer
4 byte 4 byte 1024 malloc 1024 字节返回的指针

但是,问题又来了,DeleteWeakGlobalRef 的时候,jweak 可能是任何类型,那么我们需要判断 是 byte[] 类型是我们申请的 fakeArray 两个条件,只有两个条件都成立,才能确认使我们为 bitmap 申请的 native byte[]

原文中使用了一个魔数,其实就是一个标志来帮助 DeleteWeakGlobalRef 时候,确认是我们申请的 fakeArray

所以,fakeArray 改为:

kclass_ uint32_t monitor_ size magic number native byte[] pointer
4 byte 4 byte 1024 魔数 malloc 1024 字节返回的指针

同时,原文中还提到

把这个 fake array 对象添加到 Global Ref table 中,以保证 fake array 的释放时机一定是在 DeleteWeakGlobalRef 之后

所以我们还需要将这个 jbyteArray 放入 GlobalRef 中,强引用

最终就成为了原文中的结构

kclass_ uint32_t monitor_ size magic number global ref native byte[] pointer
4 byte 4 byte 申请时需要的 1024 字节 魔数 GlobalRef malloc 返回的指针

所以,最后我们在 DeleteWeakGlobalRef 中,首先要判断是我们申请的 fake byte,然后需要把 size 替换回来(因为我们申请的 fake array size != bitmap real size),然后取到存储的 native 指针,释放 native 内存,别忘记移除 global ref。

至此整个流程基本已经完成了

这部分 github 私人开源版本和原文的实现不太一致,可以直接使用原文的方式

问题5

通过 jni 反射调用 VMRuntime 的这两个 java 方法

总结

其实这个能力,可能在不久的将来就没有了实际意义,因为 Andriod8 以后,Bitmap 内存天然分配在了 native heap

不过,目前确实还是有明确的作用,并且整个实现过程,也有一些有意思的地方,值得学习吧

不过不得不吐槽原文,很多东西都说的有偏差,并且 Android 5.x Bitmap 释放逻辑根本没有 WeakReference 相关的东西···

总之,最终踩了一些坑,实现的是 Android 6 - 7 版本的 NativeBitmap

https://github.com/PTrainbow/NativeBitmap