Android | Bitmap的Java對象GC之后,對應(yīng)的native內(nèi)存會(huì)回收嗎?

前言

  • Bitmap 的內(nèi)存分配分外兩塊:Java 堆和native 堆。我們都知道 JVM 有垃圾回收機(jī)制,那么當(dāng) Bitmap的Java對象GC之后,對應(yīng)的 native 堆內(nèi)存會(huì)回收嗎?

帶你理解 NativeAllocationRegistry 的原理與設(shè)計(jì)思想

NativeAllocationRegistryAndroid 8.0(API 27)引入的一種輔助回收native內(nèi)存的機(jī)制,使用步驟并不復(fù)雜,但是關(guān)聯(lián)的Java原理知識(shí)卻不少

  • 這篇文章將帶你理解NativeAllocationRegistry的原理,并分析相關(guān)源碼。如果能幫上忙,請務(wù)必點(diǎn)贊加關(guān)注,這真的對我非常重要。

目錄

1. 使用步驟

Android 8.0(API 27)開始,Android中很多地方可以看到NativeAllocationRegistry的身影,我們以Bitmap為例子介紹NativeAllocationRegistry的使用步驟,涉及文件:Bitmap.java、Bitmap.h、Bitmap.cpp

步驟1:創(chuàng)建 NativeAllocationRegistry

首先,我們看看實(shí)例化NativeAllocationRegistry的地方,具體在Bitmap的構(gòu)造函數(shù)中:

// # Android 8.0

// Bitmap.java

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

    // 【分析點(diǎn) 1:native 層需要的內(nèi)存大小】
    long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
    // 【分析點(diǎn) 2:回收函數(shù) nativeGetNativeFinalizer()】
    // 【分析點(diǎn) 3:加載回收函數(shù)的類加載器: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的構(gòu)造函數(shù)(在從JNI中調(diào)用)中實(shí)例化了NativeAllocationRegistry,并傳遞了三個(gè)參數(shù):

參數(shù) 解釋
classLoader 加載freeFunction函數(shù)的類加載器
freeFunction 回收native內(nèi)存的native函數(shù)直接地址
size 分配的native內(nèi)存大?。▎挝唬鹤止?jié))

步驟2:注冊對象

緊接著,調(diào)用了registerNativeAllocation(...),并傳遞兩個(gè)參數(shù):

參數(shù) 解釋
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) {
    // 代碼省略,下文補(bǔ)充...
}

步驟3:回收內(nèi)存

完成前面兩步后,當(dāng)Java層對象被垃圾回收后,NativeAllocationRegistry會(huì)自動(dòng)回收注冊的native內(nèi)存。例如,我們加載幾張圖片,隨后釋放Bitmap的引用,可以觀察到GC之后,native層的內(nèi)存也自動(dòng)回收了:

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

2. 提出問題

掌握了NativeAllocationRegistry的作用和使用步驟后,很自然地會(huì)有一些疑問:

  • 為什么在Java層對象被垃圾回收后,native內(nèi)存會(huì)自動(dòng)被回收呢?
  • NativeAllocationRegistry是從Android 8.0(API 27)開始引入,那么在此之前,native內(nèi)存是如何回收的呢?

通過分析NativeAllocationRegistry源碼,我們將一步步解答這些問題,請繼續(xù)往下看。


3. NativeAllocationRegistry 源碼分析

現(xiàn)在我們將視野回到到NativeAllocationRegistry的源碼,涉及文件:NativeAllocationRegistry.java 、NativeAllocationRegistry_Delegate.java、libcore_util_NativeAllocationRegistry.cpp

3.1 構(gòu)造函數(shù)

// NativeAllocationRegistry.java

public class NativeAllocationRegistry {
    // 加載 freeFunction 函數(shù)的類加載器
    private final ClassLoader classLoader;
    // 回收 native 內(nèi)存的 native 函數(shù)直接地址
    private final long freeFunction;
    // 分配的 native 內(nèi)存大?。ㄗ止?jié))
    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的構(gòu)造函數(shù)只是將三個(gè)參數(shù)保存下來,并沒有執(zhí)行額外操作。以Bitmap為例,三個(gè)參數(shù)在Bitmap的構(gòu)造函數(shù)中獲得,我們繼續(xù)上一節(jié)未完成的分析過程:

  • 分析點(diǎn) 1:native 層需要的內(nèi)存大小
// Bitmap.java

// 【分析點(diǎn) 1:native 層需要的內(nèi)存大小】
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;
    }
    // 調(diào)用 native 方法
    return nativeGetAllocationByteCount(mNativePtr);
}

private static final long NATIVE_ALLOCATION_SIZE = 32;

可以看到,nativeSize由固定的32字節(jié)加上getAllocationByteCount(),總之,NativeAllocationRegistry需要一個(gè)native層內(nèi)存大小的參數(shù),這里就不展開了。關(guān)于Bitmap內(nèi)存分配的詳細(xì)分析請務(wù)必閱讀文章:《Android | 各版本中 Bitmap 內(nèi)存分配對比》

  • 分析點(diǎn) 2:回收函數(shù) nativeGetNativeFinalizer()
// Bitmap.java

// 【分析點(diǎn) 2:回收函數(shù) 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) {
    // 轉(zhuǎn)為long
    return static_cast<jlong>(reinterpret_cast<uintptr_t>(&Bitmap_destruct));
}

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

可以看到,nativeGetNativeFinalizer()是一個(gè)native函數(shù),返回值是一個(gè)long,這個(gè)值其實(shí)相當(dāng)于Bitmap_destruct()函數(shù)的直接地址。很明顯,Bitmap_destruct()就是用來回收native層內(nèi)存的。

那么,Bitmap_destruct()是在哪里調(diào)用的呢?繼續(xù)往下看!

  • 分析點(diǎn) 3:加載回收函數(shù)的類加載器
// Bitmap.java
Bitmap.class.getClassLoader()

另外,NativeAllocationRegistry還需要ClassLoader參數(shù),文檔注釋指出:classloader是加載freeFunction所在native庫的類加載器,但是NativeAllocationRegistry內(nèi)部并沒有使用這個(gè)參數(shù)。這里筆者也不理解為什么需要傳遞這個(gè)參數(shù),如果有知道答案的小伙伴請告訴我一下~

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 (...)方法參數(shù)是Java層對象引用與native層對象的地址。函數(shù)體乍一看是有點(diǎn)繞,筆者在這里也停留了好長一會(huì)。我們簡化一下代碼,try-catch代碼先省略,函數(shù)返回值Runnable暫時(shí)用不到也先省略,瘦身后的代碼如下:

// NativeAllocationRegistry.java

// (簡化)
public void registerNativeAllocation(Object referent, long nativePtr) {
    CleanerThunk thunk thunk = new CleanerThunk();
    // Cleaner 綁定 Java 對象與回收函數(shù)
    Cleaner cleaner = Cleaner.create(referent, thunk);
    // 注冊 native 內(nèi)存
    registerNativeAllocation(this.size);
    thunk.setNativePtr(nativePtr);
}

private class CleanerThunk implements Runnable {
    // 代碼省略,下文補(bǔ)充...
}

看到這里,上文提出的第一個(gè)疑問就可以解釋了,原來NativeAllocationRegistry內(nèi)部是利用了sun.misc.Cleaner.java機(jī)制,簡單來說:使用虛引用得知對象被GC的時(shí)機(jī),在GC前執(zhí)行額外的回收工作。若還不了解Java的四種引用類型,請務(wù)必閱讀:《Java | 引用類型》

# 舉一反三 #

DirectByteBuffer內(nèi)部也是利用了Cleaner實(shí)現(xiàn)堆外內(nèi)存的釋放的。若不了解,請務(wù)必閱讀:《Java | 堆內(nèi)存與堆外內(nèi)存》

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

    public void run() {
        if (nativePtr != 0) {
            // 【分析點(diǎn) 4:執(zhí)行內(nèi)存回收方法】
            applyFreeFunction(freeFunction, nativePtr);
            // 【分析點(diǎn) 5:注銷 native 內(nèi)存】
            registerNativeFree(size);
        }
    }

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

繼續(xù)往下看,CleanerThunk其實(shí)是Runnable的實(shí)現(xiàn)類,run()Java層對象被垃圾回收時(shí)觸發(fā),主要做了兩件事:

  • 分析點(diǎn) 4:執(zhí)行內(nèi)存回收方法
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));
    // 調(diào)用回收函數(shù)
    nativeFreeFunction(nativePtr);
}

可以看到,applyFreeFunction(...)最終就是執(zhí)行到了前面提到的內(nèi)存回收函數(shù),對于Bitmap就是Bitmap_destruct()

  • 分析點(diǎn) 5:注冊 / 注銷native內(nèi)存
// NativeAllocationRegistry.java

// 注冊 native 內(nèi)存
registerNativeAllocation(this.size);
// 注銷 native 內(nèi)存
registerNativeFree(size);

// 提示:這一層函數(shù)其實(shí)就是為了將參數(shù)轉(zhuǎn)為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內(nèi)存,比便在內(nèi)存占用達(dá)到界限時(shí)觸發(fā)GC,在該native內(nèi)存回收時(shí),需要向VM注銷該內(nèi)存量


4. 對比 Android 8.0 之前回收 native 內(nèi)存的方式

前面我們已經(jīng)分析完NativeAllocationRegistry的源碼了,我們看一看在Android 8.0之前,Bitmap是用什么方法回收native內(nèi)存的,涉及文件: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 層內(nèi)存
            VMRuntime.getRuntime().registerNativeFree(mNativeAllocationByteCount);
        }
        mNativeAllocationByteCount = nativeByteCount;
        if (mNativeAllocationByteCount != 0) {
            // 注銷 native 層內(nèi)存
            VMRuntime.getRuntime().registerNativeAllocation(mNativeAllocationByteCount);
        }
    }

    @Override
    public void finalize() {
        try {
            super.finalize();
        } catch (Throwable t) {
            // Ignore
        } finally {
            setNativeAllocationByteCount(0);
            // 執(zhí)行內(nèi)存回收函數(shù)
            nativeDestructor(mNativeBitmap);
            mNativeBitmap = 0;
        }
    }
} 

private static native void nativeDestructor(long nativeBitmap);

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

  • 共同點(diǎn):
    • 分配的native層內(nèi)存需要向VM注冊 / 注銷
    • 通過一個(gè)native層的內(nèi)存回收函數(shù)來回收內(nèi)存
  • 不同點(diǎn):
    • NativeAllocationRegistry依賴于sun.misc.Cleaner.java
    • BitmapFinalizer依賴于Object#finalize()

我們知道,finalize()Java對象被垃圾回收時(shí)會(huì)調(diào)用,BitmapFinalizer就是利用了這個(gè)機(jī)制來回收native層內(nèi)存的。若不了解,請務(wù)必閱讀文章:《Java | 談?wù)勎覍厥盏睦斫狻?/a>

再舉幾個(gè)常用的類在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對象被回收的時(shí)機(jī),來回收native層內(nèi)存
  • Android 8.0 (API 27)之前,Android通常使用Object#finalize()調(diào)用時(shí)機(jī)來回收native層內(nèi)存

推薦閱讀

感謝喜歡!你的點(diǎn)贊是對我最大的鼓勵(lì)!歡迎關(guān)注彭旭銳的簡書!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。