
前言
- Bitmap 的內(nèi)存分配分外兩塊:Java 堆和native 堆。我們都知道 JVM 有垃圾回收機(jī)制,那么當(dāng) Bitmap的Java對(duì)象GC之后,對(duì)應(yīng)的 native 堆內(nèi)存會(huì)回收嗎?
帶你理解 NativeAllocationRegistry 的原理與設(shè)計(jì)思想
NativeAllocationRegistry是Android 8.0(API 27)引入的一種輔助回收native內(nèi)存的機(jī)制,使用步驟并不復(fù)雜,但是關(guān)聯(lián)的Java原理知識(shí)卻不少
- 這篇文章將帶你理解
NativeAllocationRegistry的原理,并分析相關(guān)源碼。如果能幫上忙,請(qǐng)務(wù)必點(diǎn)贊加關(guān)注,這真的對(duì)我非常重要。
目錄

1. 使用步驟
從Android 8.0(API 27)開(kāi)始,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ù)的類(lèi)加載器:Bitmap.class.getClassLoader()】
NativeAllocationRegistry registry = new NativeAllocationRegistry(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
// 注冊(cè) Java 層對(duì)象引用與 native 層對(duì)象的地址
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ù)的類(lèi)加載器 |
freeFunction |
回收native內(nèi)存的native函數(shù)直接地址 |
size |
分配的native內(nèi)存大?。▎挝唬鹤止?jié)) |
步驟2:注冊(cè)對(duì)象
緊接著,調(diào)用了registerNativeAllocation(...),并傳遞兩個(gè)參數(shù):
| 參數(shù) | 解釋 |
|---|---|
referent |
Java層對(duì)象的引用 |
nativeBitmap |
native層對(duì)象的地址 |
// Bitmap.java
// called from JNI
Bitmap(long nativeBitmap,...){
// 省略其他代碼...
// 注冊(cè) Java 層對(duì)象引用與 native 層對(duì)象的地址
registry.registerNativeAllocation(this, nativeBitmap);
}
// NativeAllocationRegistry.java
public Runnable registerNativeAllocation(Object referent, long nativePtr) {
// 代碼省略,下文補(bǔ)充...
}
步驟3:回收內(nèi)存
完成前面兩步后,當(dāng)Java層對(duì)象被垃圾回收后,NativeAllocationRegistry會(huì)自動(dòng)回收注冊(cè)的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. 提出問(wèn)題
掌握了NativeAllocationRegistry的作用和使用步驟后,很自然地會(huì)有一些疑問(wèn):
- 為什么在
Java層對(duì)象被垃圾回收后,native內(nèi)存會(huì)自動(dòng)被回收呢? NativeAllocationRegistry是從Android 8.0(API 27)開(kāi)始引入,那么在此之前,native內(nèi)存是如何回收的呢?
通過(guò)分析NativeAllocationRegistry源碼,我們將一步步解答這些問(wèn)題,請(qǐng)繼續(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ù)的類(lèi)加載器
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ù)保存下來(lái),并沒(méi)有執(zhí)行額外操作。以Bitmap為例,三個(gè)參數(shù)在Bitmap的構(gòu)造函數(shù)中獲得,我們繼續(xù)上一節(jié)未完成的分析過(guò)程:
- 分析點(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ù),這里就不展開(kāi)了。關(guān)于Bitmap內(nèi)存分配的詳細(xì)分析請(qǐng)務(wù)必閱讀文章:《Android | 各版本中 Bitmap 內(nèi)存分配對(duì)比》
- 分析點(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()就是用來(lái)回收native層內(nèi)存的。
那么,Bitmap_destruct()是在哪里調(diào)用的呢?繼續(xù)往下看!
- 分析點(diǎn) 3:加載回收函數(shù)的類(lèi)加載器
// Bitmap.java
Bitmap.class.getClassLoader()
另外,NativeAllocationRegistry還需要ClassLoader參數(shù),文檔注釋指出:classloader是加載freeFunction所在native庫(kù)的類(lèi)加載器,但是NativeAllocationRegistry內(nèi)部并沒(méi)有使用這個(gè)參數(shù)。這里筆者也不理解為什么需要傳遞這個(gè)參數(shù),如果有知道答案的小伙伴請(qǐng)告訴我一下~
3.2 注冊(cè)對(duì)象
// Bitmap.java
// 注冊(cè) Java 層對(duì)象引用與 native 層對(duì)象的地址
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層對(duì)象引用與native層對(duì)象的地址。函數(shù)體乍一看是有點(diǎn)繞,筆者在這里也停留了好長(zhǎng)一會(huì)。我們簡(jiǎn)化一下代碼,try-catch代碼先省略,函數(shù)返回值Runnable暫時(shí)用不到也先省略,瘦身后的代碼如下:
// NativeAllocationRegistry.java
// (簡(jiǎn)化)
public void registerNativeAllocation(Object referent, long nativePtr) {
CleanerThunk thunk thunk = new CleanerThunk();
// Cleaner 綁定 Java 對(duì)象與回收函數(shù)
Cleaner cleaner = Cleaner.create(referent, thunk);
// 注冊(cè) native 內(nèi)存
registerNativeAllocation(this.size);
thunk.setNativePtr(nativePtr);
}
private class CleanerThunk implements Runnable {
// 代碼省略,下文補(bǔ)充...
}
看到這里,上文提出的第一個(gè)疑問(wèn)就可以解釋了,原來(lái)NativeAllocationRegistry內(nèi)部是利用了sun.misc.Cleaner.java機(jī)制,簡(jiǎn)單來(lái)說(shuō):使用虛引用得知對(duì)象被GC的時(shí)機(jī),在GC前執(zhí)行額外的回收工作。若還不了解Java的四種引用類(lèi)型,請(qǐng)務(wù)必閱讀:《Java | 引用類(lèi)型》
# 舉一反三 #
DirectByteBuffer內(nèi)部也是利用了Cleaner實(shí)現(xiàn)堆外內(nèi)存的釋放的。若不了解,請(qǐng)務(wù)必閱讀:《Java | 堆內(nèi)存與堆外內(nèi)存》
private class CleanerThunk implements Runnable {
// native 層對(duì)象的地址
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:注銷(xiāo) native 內(nèi)存】
registerNativeFree(size);
}
}
public void setNativePtr(long nativePtr) {
this.nativePtr = nativePtr;
}
}
繼續(xù)往下看,CleanerThunk其實(shí)是Runnable的實(shí)現(xiàn)類(lèi),run()在Java層對(duì)象被垃圾回收時(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ù),對(duì)于Bitmap就是Bitmap_destruct()
- 分析點(diǎn) 5:注冊(cè) / 注銷(xiāo)native內(nèi)存
// NativeAllocationRegistry.java
// 注冊(cè) native 內(nèi)存
registerNativeAllocation(this.size);
// 注銷(xiāo) 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注冊(cè)native內(nèi)存,比便在內(nèi)存占用達(dá)到界限時(shí)觸發(fā)GC,在該native內(nèi)存回收時(shí),需要向VM注銷(xiāo)該內(nèi)存量
4. 對(duì)比 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) {
// 注冊(cè) native 層內(nèi)存
VMRuntime.getRuntime().registerNativeFree(mNativeAllocationByteCount);
}
mNativeAllocationByteCount = nativeByteCount;
if (mNativeAllocationByteCount != 0) {
// 注銷(xiāo) 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注冊(cè) / 注銷(xiāo) - 通過(guò)一個(gè)
native層的內(nèi)存回收函數(shù)來(lái)回收內(nèi)存
- 分配的
- 不同點(diǎn):
NativeAllocationRegistry依賴于sun.misc.Cleaner.javaBitmapFinalizer依賴于Object#finalize()
我們知道,finalize()在Java對(duì)象被垃圾回收時(shí)會(huì)調(diào)用,BitmapFinalizer就是利用了這個(gè)機(jī)制來(lái)回收native層內(nèi)存的。若不了解,請(qǐng)務(wù)必閱讀文章:《Java | 談?wù)勎覍?duì)垃圾回收的理解》
再舉幾個(gè)常用的類(lèi)在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. 問(wèn)題回歸
NativeAllocationRegistry利用虛引用感知Java對(duì)象被回收的時(shí)機(jī),來(lái)回收native層內(nèi)存- 在
Android 8.0 (API 27)之前,Android通常使用Object#finalize()調(diào)用時(shí)機(jī)來(lái)回收native層內(nèi)存
推薦閱讀
- Java | 帶你理解 ServiceLoader 的原理與設(shè)計(jì)思想
- Android | 談一談 Matrix 與坐標(biāo)變換
- Android | 一文帶你全面了解 AspectJ 框架
- Android | 使用 AspectJ 限制按鈕快速點(diǎn)擊
- Android | 這是一份詳細(xì)的 EventBus 使用教程
- 開(kāi)發(fā)者 | 淺析App社交分享的5種形式
- 計(jì)算機(jī)組成原理 | Unicode 和 UTF-8是什么關(guān)系?
- 計(jì)算機(jī)組成原理 | 為什么浮點(diǎn)數(shù)運(yùn)算不精確?(阿里筆試)
感謝喜歡!你的點(diǎn)贊是對(duì)我最大的鼓勵(lì)!歡迎關(guān)注彭旭銳的簡(jiǎn)書(shū)!
