谷歌官方改了兩次的知識(shí)點(diǎn),你一定要知道!
大家好,我是皇叔,最近開了一個(gè)安卓進(jìn)階漲薪訓(xùn)練營(yíng),可以幫助大家突破技術(shù)&職場(chǎng)瓶頸,從而度過難關(guān),進(jìn)入心儀的公司。
詳情見這篇文章:繼Android進(jìn)階三部曲之后,我的最強(qiáng)作來啦!
今天面試遇到同學(xué)說做過內(nèi)存優(yōu)化,于是我一般都會(huì)問那 Bitmap 的像素內(nèi)存存在哪?大多數(shù)同學(xué)都回答在 java heap 里面,就比較尷尬,理論上你做內(nèi)存優(yōu)化,如果連圖片這個(gè)內(nèi)存大戶內(nèi)存存在哪都不清楚,實(shí)在不太能說得過去。
Bitmap可以說是安卓里面最常見的內(nèi)存消耗大戶了,我們開發(fā)過程中遇到的oom問題很多都是由它引發(fā)的。谷歌官方也一直在迭代它的像素內(nèi)存管理策略。從 Android 2.3.3以前的分配在native上,到2.3-7.1之間的分配在java堆上,又到8.0之后的回到native上。幾度變遷,它的回收方法也在跟著變化。
Android 2.3.3以前
2.3.3以前Bitmap的像素內(nèi)存是分配在natvie上,而且不確定什么時(shí)候會(huì)被回收。根據(jù)官方文檔的說法我們需要手動(dòng)調(diào)用Bitmap.recycle()去回收:
https://developer.android.com/topic/performance/graphics/manage-memory
在 Android 2.3.3(API 級(jí)別 10)及更低版本上,位圖的后備像素?cái)?shù)據(jù)存儲(chǔ)在本地內(nèi)存中。它與存儲(chǔ)在 Dalvik 堆中的位圖本身是分開的。本地內(nèi)存中的像素?cái)?shù)據(jù)并不以可預(yù)測(cè)的方式釋放,可能會(huì)導(dǎo)致應(yīng)用短暫超出其內(nèi)存限制并崩潰。
在 Android 2.3.3(API 級(jí)別 10)及更低版本上,建議使用?recycle()。如果您在應(yīng)用中顯示大量位圖數(shù)據(jù),則可能會(huì)遇到 OutOfMemoryError 錯(cuò)誤。利用?recycle()?方法,應(yīng)用可以盡快回收內(nèi)存。
注意:只有當(dāng)您確定位圖已不再使用時(shí)才應(yīng)該使用?recycle()。如果您調(diào)用?recycle()?并在稍后嘗試?yán)L制位圖,則會(huì)收到錯(cuò)誤:"Canvas: trying to use a recycled bitmap"。
2.Android 3.0~Android 7.1
雖然3.0~7.1的版本Bitmp的像素內(nèi)存是分配在java堆上的,但是實(shí)際是在natvie層進(jìn)行decode的,而且會(huì)在native層創(chuàng)建一個(gè)c++的對(duì)象和java層的Bitmap對(duì)象進(jìn)行關(guān)聯(lián)。
從BitmapFactory的源碼我們可以看到它一路調(diào)用到nativeDecodeStream這個(gè)native方法:
//?BitmapFactory.java
public?static?Bitmap?decodeFile(String?pathName,?Options?opts)?{
????...
????stream?=?new?FileInputStream(pathName);
????bm?=?decodeStream(stream,?null,?opts);
????...
????return?bm;
}
public?static?Bitmap?decodeStream(InputStream?is,?Rect?outPadding,?Options?opts)?{
????...
????bm?=?decodeStreamInternal(is,?outPadding,?opts);
????...
????return?bm;
}
private?static?Bitmap?decodeStreamInternal(InputStream?is,?Rect?outPadding,?Options?opts)?{
????...
????return?nativeDecodeStream(is,?tempStorage,?outPadding,?opts);
}
nativeDecodeStream實(shí)際上會(huì)通過jni創(chuàng)建java堆的內(nèi)存,然后讀取io流解碼圖片將像素?cái)?shù)據(jù)存到這個(gè)java堆內(nèi)存里面:
//?BitmapFactory.cpp
static?jobject?nativeDecodeStream(JNIEnv*?env,?jobject?clazz,?jobject?is,?jbyteArray?storage,
????????jobject?padding,?jobject?options)?{
????...
????bitmap?=?doDecode(env,?bufferedStream,?padding,?options);
????...
????return?bitmap;
}
static?jobject?doDecode(JNIEnv*?env,?SkStreamRewindable*?stream,?jobject?padding,?jobject?options)?{
????...
????//?outputAllocator是像素內(nèi)存的分配器,會(huì)在java堆上創(chuàng)建內(nèi)存給像素?cái)?shù)據(jù),可以通過BitmapFactory.Options.inBitmap復(fù)用前一個(gè)bitmap像素內(nèi)存
????SkBitmap::Allocator*?outputAllocator?=?(javaBitmap?!=?NULL)??
????????????(SkBitmap::Allocator*)&recyclingAllocator?:?(SkBitmap::Allocator*)&javaAllocator;
????...
????//?將內(nèi)存分配器設(shè)置給解碼器
????decoder->setAllocator(outputAllocator);
????...
????//解碼
????if?(decoder->decode(stream,?&decodingBitmap,?prefColorType,?decodeMode)
????????????????!=?SkImageDecoder::kSuccess)?{
????????return?nullObjectReturn("decoder->decode?returned?false");
????}
????...
????return?GraphicsJNI::createBitmap(env,?javaAllocator.getStorageObjAndReset(),
????????????bitmapCreateFlags,?ninePatchChunk,?ninePatchInsets,?-1);
}
//?Graphics.cpp
jobject?GraphicsJNI::createBitmap(JNIEnv*?env,?android::Bitmap*?bitmap,
????????int?bitmapCreateFlags,?jbyteArray?ninePatchChunk,?jobject?ninePatchInsets,
????????int?density)?{
????//?java層的Bitmap對(duì)象實(shí)際上是natvie層new出來的
????//?native層也會(huì)創(chuàng)建一個(gè)android::Bitmap對(duì)象與java層的Bitmap對(duì)象綁定
????//?bitmap->javaByteArray()代碼bitmap的像素?cái)?shù)據(jù)其實(shí)是存在java層的byte數(shù)組中
????jobject?obj?=?env->NewObject(gBitmap_class,?gBitmap_constructorMethodID,
????????????reinterpret_cast(bitmap),?bitmap->javaByteArray(),
????????????bitmap->width(),?bitmap->height(),?density,?isMutable,?isPremultiplied,
????????????ninePatchChunk,?ninePatchInsets);
????...
????return?obj;
}
我們可以看最后會(huì)調(diào)用javaAllocator.getStorageObjAndReset()創(chuàng)建一個(gè)android::Bitmap類型的native層Bitmap對(duì)象,然后通過jni調(diào)用java層的Bitmap構(gòu)造函數(shù)去創(chuàng)建java層的Bitmap對(duì)象,同時(shí)將native層的Bitmap對(duì)象保存到mNativePtr:
//?Bitmap.java
//?Convenience?for?JNI?access
private?final?long?mNativePtr;
/**
?*?Private?constructor?that?must?received?an?already?allocated?native?bitmap
?*?int?(pointer).
?*/
//?called?from?JNI
Bitmap(long?nativeBitmap,?byte[]?buffer,?int?width,?int?height,?int?density,
????????boolean?isMutable,?boolean?requestPremultiplied,
????????byte[]?ninePatchChunk,?NinePatch.InsetStruct?ninePatchInsets)?{
????...
????mNativePtr?=?nativeBitmap;
????...
}
從上面的源碼我們也能看出來,Bitmap的像素是存在java堆的,所以如果bitmap沒有人使用了,垃圾回收器就能自動(dòng)回收這塊的內(nèi)存,但是在native創(chuàng)建出來的nativeBitmap要怎么回收呢?從Bitmap的源碼我們可以看到在Bitmap構(gòu)造函數(shù)里面還會(huì)創(chuàng)建一個(gè)BitmapFinalizer去管理nativeBitmap:
/**
?*?Private?constructor?that?must?received?an?already?allocated?native?bitmap
?*?int?(pointer).
?*/
//?called?from?JNI
Bitmap(long?nativeBitmap,?byte[]?buffer,?int?width,?int?height,?int?density,
????????boolean?isMutable,?boolean?requestPremultiplied,
????????byte[]?ninePatchChunk,?NinePatch.InsetStruct?ninePatchInsets)?{
????...
????mNativePtr?=?nativeBitmap;
????mFinalizer?=?new?BitmapFinalizer(nativeBitmap);
????...
}
BitmapFinalizer的原理十分簡(jiǎn)單。Bitmap對(duì)象被銷毀的時(shí)候BitmapFinalizer也會(huì)同步被銷毀,然后就可以在BitmapFinalizer.finalize()里面銷毀native層的nativeBitmap:
private?static?class?BitmapFinalizer?{
????private?long?mNativeBitmap;
????...
????BitmapFinalizer(long?nativeBitmap)?{
????????mNativeBitmap?=?nativeBitmap;
????}
????...
????@Override
????public?void?finalize()?{
????????try?{
????????????super.finalize();
????????}?catch?(Throwable?t)?{
????????????//?Ignore
????????}?finally?{
????????????setNativeAllocationByteCount(0);
????????????nativeDestructor(mNativeBitmap);
????????????mNativeBitmap?=?0;
????????}
????}
}3.Android 8.0之后
8.0以后像素內(nèi)存又被放回了native上,所以依然需要在java層的Bitmap對(duì)象回收之后同步回收native的內(nèi)存。
雖然BitmapFinalizer同樣可以實(shí)現(xiàn),但是Java的finalize方法實(shí)際上是不推薦使用的,所以谷歌也換了NativeAllocationRegistry去實(shí)現(xiàn):
/**
?*?Private?constructor?that?must?received?an?already?allocated?native?bitmap
?*?int?(pointer).
?*/
//?called?from?JNI
Bitmap(long?nativeBitmap,?int?width,?int?height,?int?density,
????????boolean?isMutable,?boolean?requestPremultiplied,
????...
????mNativePtr?=?nativeBitmap;
????long?nativeSize?=?NATIVE_ALLOCATION_SIZE?+?getAllocationByteCount();
????NativeAllocationRegistry?registry?=?new?NativeAllocationRegistry(
????????Bitmap.class.getClassLoader(),?nativeGetNativeFinalizer(),?nativeSize);
????registry.registerNativeAllocation(this,?nativeBitmap);
}
NativeAllocationRegistry底層實(shí)際上使用了sun.misc.Cleaner,可以為對(duì)象注冊(cè)一個(gè)清理的Runnable。當(dāng)對(duì)象內(nèi)存被回收的時(shí)候jvm就會(huì)調(diào)用它。
import?sun.misc.Cleaner;
public?Runnable?registerNativeAllocation(Object?referent,?Allocator?allocator)?{
????...
????CleanerThunk?thunk?=?new?CleanerThunk();
????Cleaner?cleaner?=?Cleaner.create(referent,?thunk);
????..
}
private?class?CleanerThunk?implements?Runnable?{
????...
????public?void?run()?{
????????if?(nativePtr?!=?0)?{
????????????applyFreeFunction(freeFunction,?nativePtr);
????????}
????????registerNativeFree(size);
????}
????...
}這個(gè)Cleaner的原理也很暴力,首先它是一個(gè)虛引用,registerNativeAllocation實(shí)際上創(chuàng)建了一個(gè)Bitmap的虛引用:
//?Cleaner.java
public?class?Cleaner?extends?PhantomReference?{
????...
????public?static?Cleaner?create(Object?ob,?Runnable?thunk)?{
????????...
????????return?add(new?Cleaner(ob,?thunk));
????}
????...
????private?Cleaner(Object?referent,?Runnable?thunk)?{
????????super(referent,?dummyQueue);
????????this.thunk?=?thunk;
????}
????...
????public?void?clean()?{
????????...
????????thunk.run();
????????...
????}
????...
}
虛引用的話我們都知道需要配合一個(gè)ReferenceQueue使用,當(dāng)對(duì)象的引用被回收的時(shí)候,jvm就會(huì)將這個(gè)虛引用丟到ReferenceQueue里面。而ReferenceQueue在插入的時(shí)候居然通過instanceof判斷了下是不是Cleaner:
//?ReferenceQueue.java
private?boolean?enqueueLocked(Reference?extends?T>?r)?{
????...
????if?(r?instanceof?Cleaner)?{
????????Cleaner?cl?=?(sun.misc.Cleaner)?r;
????????cl.clean();
????????...
????}
????...
}
也就是說Bitmap對(duì)象被回收,就會(huì)觸發(fā)Cleaner這個(gè)虛引用被丟入ReferenceQueue,而ReferenceQueue里面會(huì)判斷丟進(jìn)來的虛引用是不是Cleaner,如果是就調(diào)用Cleaner.clean()方法。而clean方法內(nèi)部就會(huì)再去執(zhí)行我們注冊(cè)的清理的Runnable。
為了防止失聯(lián),歡迎關(guān)注我的小號(hào)
?? 微信改了推送機(jī)制,真愛請(qǐng)星標(biāo)本公號(hào)??



