前言
NativeAllocationRegistry
是Android 8.0(API 27)
引入的一种辅助回收native
内存的机制,使用步骤并不复杂,但是关联的Java
原理知识却不少- 这篇文章将带你理解
NativeAllocationRegistry
的原理,并分析相关源码。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
目录
1. 使用步骤
从Android 8.0(API 27)
开始,Android
中很多地方可以看到NativeAllocationRegistry
的身影,我们以Bitmap
为例子介绍NativeAllocationRegistry
的使用步骤,涉及文件:Bitmap.java、Bitmap.h、Bitmap.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.java 、NativeAllocationRegistry_Delegate.java、libcore_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
层内存