Bitmap

Bitmap像素存储

03 | 内存优化(上):4GB内存时代,再谈内存优化

Android Bitmap变迁与原理解析(4.x-8.x)

Bitmap: 从出生到死亡

Bitmap创建

image

image

Java 层的创建 Bitmap 的所有 API 进入到 Native 层后,全都会走如下这四个步骤。

  1. ==资源转换== - 这一步将 Java 层传来的不同类型的资源转换成解码器可识别的数据类型
  2. ==内存分配== - 分配内存时会考虑是否复用 Bitmap、是否缩放 Bitmap 等因素
  3. ==图片解码== - 实际的解码工作由第三方库完成,解码结果填在上一步分配的内存中。注,Bitmap.createBitmap() 和 Bitmap.copy() 创建的 Bitmap 不需要进行图片解码
  4. ==创建对象== - 这一步将包含解码数据的内存块包装成 Java 层的 android.graphics.Bitmap 对象,方便 App 使用

1. 资源转换

image

2. 内存分配

image

3. 图片解码

image

image

创建Java对象

image

Bitmap销毁

Bitmap.recycle()

image

自动释放:NativeAllocationRegistry

NativeAllocationRegistry 用于将 native 内存跟 Java 对象关联,并将它们注册到 Java 运行时。注册 Java 对象关联的 native 内存有几个好处:

  • Java 运行时在 GC 调度时可考虑 native 内存状态
  • Java 运行时在 Java 对象变得不可达时可以使用用户提供的函数来自动清理 native 内存

当 Java 层 Bitmap 对象不可达后关联的 native 内存会由 nativeGetNativeFinalizer() 指定的方法来回收

static void Bitmap_destruct(BitmapWrapper* bitmap) {
    delete bitmap;
}

static jlong Bitmap_getNativeFinalizer(JNIEnv*, jobject) {
    return static_cast<jlong>(reinterpret_cast<uintptr_t>(&Bitmap_destruct));
}

//we must ensure to not leak java Bitmap Object, this will recycle bitmap memory in native around GC, while it cannot be reclaim if the java bitmap is leak.

//下图流程稍有问题,实测为ReferenceQueueDaemon便利enqueue过程会直接调用Cleaner.clean开启流程,没有使用到VMRuntime和CleanerRuner,具体流程见BitmapSource

image

image

Bitmap内存分配原理

8.0之前Bitmap内存分配原理

通过Bitmap的成员列表,就能看出一点眉目,Bitmap中有个byte[] mBuffer,其实就是用来存储像素数据的,很明显它位于java heap中:

public final class Bitmap implements Parcelable {
    private static final String TAG = "Bitmap";
     ...
    private byte[] mBuffer;
     ...
    }

image

Java层Bitmap的创建最终还是会走向native层:Bitmap.cpp

 static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                               jint offset, jint stride, jint width, jint height,
                               jint configHandle, jboolean isMutable) {
     SkColorType colorType = GraphicsJNI::legacyBitmapConfigToColorType(configHandle);
      ... 
 
     SkBitmap Bitmap;
     Bitmap.setInfo(SkImageInfo::Make(width, height, colorType, kPremul_SkAlphaType));
   		<!--关键点1 像素内存分配-->
     Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, &Bitmap, NULL);
     if (!nativeBitmap) {
         return NULL;
     }
      ... 
     <!--获取分配地址-->
     jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj);
     ...
     <!--创建Bitmap-->
     android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr,
             info, rowBytes, ctable);
     wrapper->getSkBitmap(Bitmap);
     Bitmap->lockPixels();
     return wrapper;
 }

这里只看关键点1,像素内存的分配:GraphicsJNI::allocateJavaPixelRef从这个函数名可以就可以看出,是在Java层分配,跟进去,也确实如此

由于只关心内存分配里其实就是在native层创建Java层byte[],并将这个byte[]作为像素存储结构,之后再通过在native层构建Java Bitmap对象的方式,将生成的byte[]传递给Bitmap.java对象:

jobject GraphicsJNI::createBitmap(JNIEnv* env, android::Bitmap* bitmap,
        int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
        int density) {
   	...<!--关键点1,构建java Bitmap对象,并设置byte[] mBuffer-->
    jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
            reinterpret_cast<jlong>(bitmap), bitmap->javaByteArray(),
            bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied,
            ninePatchChunk, ninePatchInsets);
    hasException(env); // For the side effect of logging.
    return obj;
}

8.0之后Bitmap内存分配

其实从8.0的Bitmap.java类也能看出区别,之前的 private byte[] mBuffer成员不见了,取而代之的是private final long mNativePtr,也就说,Bitmap.java只剩下一个壳了,具体如下:

public final class Bitmap implements Parcelable {
    ...
    // Convenience for JNI access
    private final long mNativePtr;
    ...
 }

之前说过8.0之后的内存分配是在native,具体到代码是怎么样的表现呢?流程与8.0之前基本类似,区别在native分配时: image

static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                              jint offset, jint stride, jint width, jint height,
                              jint configHandle, jboolean isMutable,
                              jfloatArray xyzD50, jobject transferParameters) {
    SkColorType colorType = GraphicsJNI::legacyBitmapConfigToColorType(configHandle);
   	 ...
   	 <!--关键点1 native层创建bitmap,并分配native内存-->
    sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(&Bitmap);
    if (!nativeBitmap) {
        return NULL;
    }
    ...
    return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable));
}

看一下allocateHeapBitmap如何分配内存

static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes) {
   	<!--关键点1 直接calloc分配内存-->
    void* addr = calloc(size, 1);
    if (!addr) {
        return nullptr;
    }
 	<!--关键点2 创建native Bitmap-->
    return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes));
}

可以看出,8.0之后,Bitmap像素内存的分配是在native层直接调用calloc,所以其像素分配的是在native heap上, 这也是为什么8.0之后的Bitmap消耗内存可以无限增长,直到耗尽系统内存,也不会提示Java OOM的原因。

8.0之后的Bitmap内存回收机制

NativeAllocationRegistry是Android 8.0引入的一种辅助自动回收native内存的一种机制,==当Java对象因为GC被回收后,NativeAllocationRegistry可以辅助回收Java对象所申请的native内存==,拿Bitmap为例,入下:

Bitmap(long nativeBitmap, int width, int height, int density,
        boolean isMutable, boolean requestPremultiplied,
        byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
    ...
    mNativePtr = nativeBitmap;
    long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
    <!--辅助回收native内存-->
    NativeAllocationRegistry registry = new NativeAllocationRegistry(
        Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
    registry.registerNativeAllocation(this, nativeBitmap);
   if (ResourcesImpl.TRACE_FOR_DETAILED_PRELOAD) {
        sPreloadTracingNumInstantiatedBitmaps++;
        sPreloadTracingTotalBitmapsSize += nativeSize;
    }
}

当然这个功能也要Java虚拟机的支持,有机会再分析。