Android | Bitmap的Java对象GC之后,对应的native内存会回收吗?

前言

  • Bitmap 的内存分配分外两块:Java 堆和native 堆。我们都知道 JVM 有垃圾回收机制,那么当 Bitmap的Java对象GC之后,对应的 native 堆内存会回收吗?

带你理解 NativeAllocationRegistry 的原理与设计思想

NativeAllocationRegistryAndroid 8.0(API 27)引入的一种辅助回收native内存的机制,使用步骤并不复杂,但是关联的Java原理知识却不少

  • 这篇文章将带你理解NativeAllocationRegistry的原理,并分析相关源码。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

目录

1. 使用步骤

Android 8.0(API 27)开始,Android中很多地方可以看到NativeAllocationRegistry的身影,我们以Bitmap为例子介绍NativeAllocationRegistry的使用步骤,涉及文件:Bitmap.javaBitmap.hBitmap.cpp

步骤1:创建 NativeAllocationRegistry

首先,我们看看实例化NativeAllocationRegistry的地方,具体在Bitmap的构造函数中:

// # Android 8.0

// Bitmap.java

// called from JNI
Bitmap(long nativeBitmap,...){
    // 省略其他代码...

    // 【分析点 1:native 层需要的内存大小】
    long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
    // 【分析点 2:回收函数 nativeGetNativeFinalizer()】
    // 【分析点 3:加载回收函数的类加载器:Bitmap.class.getClassLoader()】
    NativeAllocationRegistry registry = new NativeAllocationRegistry(
        Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
    // 注册 Java 层对象引用与 native 层对象的地址
    registry.registerNativeAllocation(this, nativeBitmap);
}

private static final long NATIVE_ALLOCATION_SIZE = 32;
private static native long nativeGetNativeFinalizer();

可以看到,Bitmap的构造函数(在从JNI中调用)中实例化了NativeAllocationRegistry,并传递了三个参数:

参数 解释
classLoader 加载freeFunction函数的类加载器
freeFunction 回收native内存的native函数直接地址
size 分配的native内存大小(单位:字节)

步骤2:注册对象

紧接着,调用了registerNativeAllocation(...),并传递两个参数:

参数 解释
referent Java层对象的引用
nativeBitmap native层对象的地址
// Bitmap.java

// called from JNI
Bitmap(long nativeBitmap,...){
    // 省略其他代码...
    // 注册 Java 层对象引用与 native 层对象的地址
    registry.registerNativeAllocation(this, nativeBitmap);
}

// NativeAllocationRegistry.java

public Runnable registerNativeAllocation(Object referent, long nativePtr) {
    // 代码省略,下文补充...
}

步骤3:回收内存

完成前面两步后,当Java层对象被垃圾回收后,NativeAllocationRegistry会自动回收注册的native内存。例如,我们加载几张图片,随后释放Bitmap的引用,可以观察到GC之后,native层的内存也自动回收了:

tv.setOnClickListener{
    val map = HashSet<Any>()
    for(index in 0 .. 2){
        map.add(BitmapFactory.decodeResource(resources,R.drawable.test))
    }
  • GC 前的内存分配情况 —— Android 8.0
  • GC 后的内存分配情况 —— Android 8.0

2. 提出问题

掌握了NativeAllocationRegistry的作用和使用步骤后,很自然地会有一些疑问:

  • 为什么在Java层对象被垃圾回收后,native内存会自动被回收呢?
  • NativeAllocationRegistry是从Android 8.0(API 27)开始引入,那么在此之前,native内存是如何回收的呢?

通过分析NativeAllocationRegistry源码,我们将一步步解答这些问题,请继续往下看。


3. NativeAllocationRegistry 源码分析

现在我们将视野回到到NativeAllocationRegistry的源码,涉及文件:NativeAllocationRegistry.javaNativeAllocationRegistry_Delegate.javalibcore_util_NativeAllocationRegistry.cpp

3.1 构造函数

// NativeAllocationRegistry.java

public class NativeAllocationRegistry {
    // 加载 freeFunction 函数的类加载器
    private final ClassLoader classLoader;
    // 回收 native 内存的 native 函数直接地址
    private final long freeFunction;
    // 分配的 native 内存大小(字节)
    private final long size;
        
    public NativeAllocationRegistry(ClassLoader classLoader, long freeFunction, long size) {
        if (size < 0) {
            throw new IllegalArgumentException("Invalid native allocation size: " + size);
        }

        this.classLoader = classLoader;
        this.freeFunction = freeFunction;
        this.size = size;
    }
}

可以看到,NativeAllocationRegistry的构造函数只是将三个参数保存下来,并没有执行额外操作。以Bitmap为例,三个参数在Bitmap的构造函数中获得,我们继续上一节未完成的分析过程:

  • 分析点 1:native 层需要的内存大小
// Bitmap.java

// 【分析点 1:native 层需要的内存大小】
long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();

public final int getAllocationByteCount() {
    if (mRecycled) {
        Log.w(TAG, "Called getAllocationByteCount() on a recycle()'d bitmap! "
                    + "This is undefined behavior!");
        return 0;
    }
    // 调用 native 方法
    return nativeGetAllocationByteCount(mNativePtr);
}

private static final long NATIVE_ALLOCATION_SIZE = 32;

可以看到,nativeSize由固定的32字节加上getAllocationByteCount(),总之,NativeAllocationRegistry需要一个native层内存大小的参数,这里就不展开了。关于Bitmap内存分配的详细分析请务必阅读文章:《Android | 各版本中 Bitmap 内存分配对比》

  • 分析点 2:回收函数 nativeGetNativeFinalizer()
// Bitmap.java

// 【分析点 2:回收函数 nativeGetNativeFinalizer()】
NativeAllocationRegistry registry = new NativeAllocationRegistry(
    Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);

private static native long nativeGetNativeFinalizer();

// Java 层
// ----------------------------------------------------------------------
// native 层

// Bitmap.cpp
static jlong Bitmap_getNativeFinalizer(JNIEnv*, jobject) {
    // 转为long
    return static_cast<jlong>(reinterpret_cast<uintptr_t>(&Bitmap_destruct));
}

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

可以看到,nativeGetNativeFinalizer()是一个native函数,返回值是一个long,这个值其实相当于Bitmap_destruct()函数的直接地址。很明显,Bitmap_destruct()就是用来回收native层内存的。

那么,Bitmap_destruct()是在哪里调用的呢?继续往下看!

  • 分析点 3:加载回收函数的类加载器
// Bitmap.java
Bitmap.class.getClassLoader()

另外,NativeAllocationRegistry还需要ClassLoader参数,文档注释指出:classloader是加载freeFunction所在native库的类加载器,但是NativeAllocationRegistry内部并没有使用这个参数。这里笔者也不理解为什么需要传递这个参数,如果有知道答案的小伙伴请告诉我一下~

3.2 注册对象

// Bitmap.java

// 注册 Java 层对象引用与 native 层对象的地址
registry.registerNativeAllocation(this, nativeBitmap);

// NativeAllocationRegistry.java

public Runnable registerNativeAllocation(Object referent, long nativePtr) {
    if (referent == null) {
        throw new IllegalArgumentException("referent is null");
    }
    if (nativePtr == 0) {
        throw new IllegalArgumentException("nativePtr is null");
    }

    CleanerThunk thunk;
    CleanerRunner result;
    try {
        thunk = new CleanerThunk();
        Cleaner cleaner = Cleaner.create(referent, thunk);
        result = new CleanerRunner(cleaner);
        registerNativeAllocation(this.size);
    } catch (VirtualMachineError vme /* probably OutOfMemoryError */) {
        applyFreeFunction(freeFunction, nativePtr);
        throw vme;
        // Other exceptions are impossible.
        // Enable the cleaner only after we can no longer throw anything, including OOME.
        thunk.setNativePtr(nativePtr);
        return result;
}

可以看到,registerNativeAllocation (...)方法参数是Java层对象引用与native层对象的地址。函数体乍一看是有点绕,笔者在这里也停留了好长一会。我们简化一下代码,try-catch代码先省略,函数返回值Runnable暂时用不到也先省略,瘦身后的代码如下:

// NativeAllocationRegistry.java

// (简化)
public void registerNativeAllocation(Object referent, long nativePtr) {
    CleanerThunk thunk thunk = new CleanerThunk();
    // Cleaner 绑定 Java 对象与回收函数
    Cleaner cleaner = Cleaner.create(referent, thunk);
    // 注册 native 内存
    registerNativeAllocation(this.size);
    thunk.setNativePtr(nativePtr);
}

private class CleanerThunk implements Runnable {
    // 代码省略,下文补充...
}

看到这里,上文提出的第一个疑问就可以解释了,原来NativeAllocationRegistry内部是利用了sun.misc.Cleaner.java机制,简单来说:使用虚引用得知对象被GC的时机,在GC前执行额外的回收工作。若还不了解Java的四种引用类型,请务必阅读:《Java | 引用类型》

# 举一反三 #

DirectByteBuffer内部也是利用了Cleaner实现堆外内存的释放的。若不了解,请务必阅读:《Java | 堆内存与堆外内存》

private class CleanerThunk implements Runnable {
    // native 层对象的地址
    private long nativePtr;
        
    public CleanerThunk() {
        this.nativePtr = 0;
    }

    public void run() {
        if (nativePtr != 0) {
            // 【分析点 4:执行内存回收方法】
            applyFreeFunction(freeFunction, nativePtr);
            // 【分析点 5:注销 native 内存】
            registerNativeFree(size);
        }
    }

    public void setNativePtr(long nativePtr) {
        this.nativePtr = nativePtr;
    }
}

继续往下看,CleanerThunk其实是Runnable的实现类,run()Java层对象被垃圾回收时触发,主要做了两件事:

  • 分析点 4:执行内存回收方法
public static native void applyFreeFunction(long freeFunction, long nativePtr);

// NativeAllocationRegistry.cpp

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);
}

可以看到,applyFreeFunction(...)最终就是执行到了前面提到的内存回收函数,对于Bitmap就是Bitmap_destruct()

  • 分析点 5:注册 / 注销native内存
// NativeAllocationRegistry.java

// 注册 native 内存
registerNativeAllocation(this.size);
// 注销 native 内存
registerNativeFree(size);

// 提示:这一层函数其实就是为了将参数转为long
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));
}

VM注册native内存,比便在内存占用达到界限时触发GC,在该native内存回收时,需要向VM注销该内存量


4. 对比 Android 8.0 之前回收 native 内存的方式

前面我们已经分析完NativeAllocationRegistry的源码了,我们看一看在Android 8.0之前,Bitmap是用什么方法回收native内存的,涉及文件:Bitmap.java (before Android 8.0)

// before Android 8.0

// Bitmap.java

private final long mNativePtr;
private final BitmapFinalizer mFinalizer;

// called from JNI
Bitmap(long nativeBitmap,...){
    // 省略其他代码...
    mNativePtr = nativeBitmap;
    mFinalizer = new BitmapFinalizer(nativeBitmap);
    int nativeAllocationByteCount = (buffer == null ? getByteCount() : 0);
    mFinalizer.setNativeAllocationByteCount(nativeAllocationByteCount);
}

private static class BitmapFinalizer {
    private long mNativeBitmap;

    private int mNativeAllocationByteCount;

    BitmapFinalizer(long nativeBitmap) {
        mNativeBitmap = nativeBitmap;
    }

    public void setNativeAllocationByteCount(int nativeByteCount) {
        if (mNativeAllocationByteCount != 0) {
            // 注册 native 层内存
            VMRuntime.getRuntime().registerNativeFree(mNativeAllocationByteCount);
        }
        mNativeAllocationByteCount = nativeByteCount;
        if (mNativeAllocationByteCount != 0) {
            // 注销 native 层内存
            VMRuntime.getRuntime().registerNativeAllocation(mNativeAllocationByteCount);
        }
    }

    @Override
    public void finalize() {
        try {
            super.finalize();
        } catch (Throwable t) {
            // Ignore
        } finally {
            setNativeAllocationByteCount(0);
            // 执行内存回收函数
            nativeDestructor(mNativeBitmap);
            mNativeBitmap = 0;
        }
    }
} 

private static native void nativeDestructor(long nativeBitmap);

如果理解了NativeAllocationRegistry的源码,上面这段代码就很好理解呀!

  • 共同点:
    • 分配的native层内存需要向VM注册 / 注销
    • 通过一个native层的内存回收函数来回收内存
  • 不同点:
    • NativeAllocationRegistry依赖于sun.misc.Cleaner.java
    • BitmapFinalizer依赖于Object#finalize()

我们知道,finalize()Java对象被垃圾回收时会调用,BitmapFinalizer就是利用了这个机制来回收native层内存的。若不了解,请务必阅读文章:《Java | 谈谈我对垃圾回收的理解》

再举几个常用的类在Android 8.0之前的源码为例子,原理都大同小异:Matrix.java (before Android 8.0)Canvas.java (before Android 8.0)

// Matrix.java

@Override
protected void finalize() throws Throwable {
    try {
        finalizer(native_instance);
    } finally {
        super.finalize();
    }
}
private static native void finalizer(long native_instance);

// Canvas.java

private final CanvasFinalizer mFinalizer;
private static final class CanvasFinalizer {
    private long mNativeCanvasWrapper;

    public CanvasFinalizer(long nativeCanvas) {
        mNativeCanvasWrapper = nativeCanvas;
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            dispose();
        } finally {
            super.finalize();
        }
    }

    public void dispose() {
        if (mNativeCanvasWrapper != 0) {
            finalizer(mNativeCanvasWrapper);
            mNativeCanvasWrapper = 0;
        }
    }
}

public Canvas() {
    // 省略其他代码...
    mFinalizer = new CanvasFinalizer(mNativeCanvasWrapper);
}

5. 问题回归

  • NativeAllocationRegistry利用虚引用感知Java对象被回收的时机,来回收native层内存
  • Android 8.0 (API 27)之前,Android通常使用Object#finalize()调用时机来回收native层内存

推荐阅读

感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的简书!

推荐阅读更多精彩内容