前言
- Bitmap 的內(nèi)存分配分外兩塊:Java 堆和native 堆。我們都知道 JVM 有垃圾回收機(jī)制,那么當(dāng) Bitmap的Java對象GC之后,對應(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)源碼。如果能幫上忙,請務(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)存
推薦閱讀
- Java | 帶你理解 ServiceLoader 的原理與設(shè)計(jì)思想
- Android | 談一談 Matrix 與坐標(biāo)變換
- Android | 一文帶你全面了解 AspectJ 框架
- Android | 使用 AspectJ 限制按鈕快速點(diǎn)擊
- Android | 這是一份詳細(xì)的 EventBus 使用教程
- 開發(fā)者 | 淺析App社交分享的5種形式
- 計(jì)算機(jī)組成原理 | Unicode 和 UTF-8是什么關(guān)系?
- 計(jì)算機(jī)組成原理 | 為什么浮點(diǎn)數(shù)運(yùn)算不精確?(阿里筆試)