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

简介: Android | 带你理解 NativeAllocationRegistry 的原理与设计思想

前言


  • NativeAllocationRegistryAndroid 8.0(API 27)引入的一种辅助回收native内存的机制,使用步骤并不复杂,但是关联的Java原理知识却不少
  • 这篇文章将带你理解NativeAllocationRegistry的原理,并分析相关源码。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。


目录

image.png

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

image.png


  • GC 后的内存分配情况 —— Android 8.0

image.png

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层内存
目录
相关文章
|
设计模式 算法 前端开发
Android面经分享,失业两个月,五一节前拿到Offer,设计思想与代码质量优化+程序性能优化+开发效率优化
Android面经分享,失业两个月,五一节前拿到Offer,设计思想与代码质量优化+程序性能优化+开发效率优化
|
安全 Shell Android开发
Android系统 init.rc sys/class系统节点写不进解决方案和原理分析
Android系统 init.rc sys/class系统节点写不进解决方案和原理分析
1419 0
|
安全 Android开发
Android13 Root实现和原理分析
Android13 Root实现和原理分析
2094 0
|
安全 Android开发 Kotlin
Android经典实战之SurfaceView原理和实践
本文介绍了 `SurfaceView` 这一强大的 UI 组件,尤其适合高性能绘制任务,如视频播放和游戏。文章详细讲解了 `SurfaceView` 的原理、与 `Surface` 类的关系及其实现示例,并强调了使用时需注意的线程安全、生命周期管理和性能优化等问题。
767 8
|
Android开发 移动开发 小程序
binder机制原理面试,安卓app开发教程
binder机制原理面试,安卓app开发教程
binder机制原理面试,安卓app开发教程
|
缓存 Java 数据库
Android的ANR原理
【10月更文挑战第18天】了解 ANR 的原理对于开发高质量的 Android 应用至关重要。通过合理的设计和优化,可以有效避免 ANR 的发生,提升应用的性能和用户体验。
808 56
|
存储 Java Android开发
Android系统 设置第三方应用为默认Launcher实现和原理分析
Android系统 设置第三方应用为默认Launcher实现和原理分析
2837 0
|
XML 前端开发 Android开发
Android View的绘制流程和原理详细解说
Android View的绘制流程和原理详细解说
533 3
|
编解码 前端开发 Android开发
Android经典实战之TextureView原理和高级用法
本文介绍了 `TextureView` 的原理和特点,包括其硬件加速渲染的优势及与其他视图叠加使用的灵活性,并提供了视频播放和自定义绘制的示例代码。通过合理管理生命周期和资源,`TextureView` 可实现高效流畅的图形和视频渲染。
1151 12
|
ARouter 测试技术 API
Android经典面试题之组件化原理、优缺点、实现方法?
本文介绍了组件化在Android开发中的应用,详细阐述了其原理、优缺点及实现方式,包括模块化、接口编程、依赖注入、路由机制等内容,并提供了具体代码示例。
474 3

热门文章

最新文章