來源:公眾號劉望舒
該文章由社區志願者張潤迪推薦
今天面試遇到同學說做過內存優化,於是我一般都會問那 Bitmap 的像素內存存在哪?大多數同學都回答在 java heap 裡面,就比較尷尬,理論上你做內存優化,如果連圖片這個內存大戶內存存在哪都不清楚,實在不太能說得過去。
Bitmap可以說是安卓裡面最常見的內存消耗大戶了,我們開發過程中遇到的oom問題很多都是由它引發的。谷歌官方也一直在迭代它的像素內存管理策略。從 Android 2.3.3以前的分配在native上,到2.3-7.1之間的分配在java堆上,又到8.0之後的回到native上。幾度變遷,它的回收方法也在跟着變化。
2.3.3以前Bitmap的像素內存是分配在natvie上,而且不確定什麼時候會被回收。根據官方文檔的說法我們需要手動調用Bitmap.recycle()去回收:
https://developer.android.com/topic/performance/graphics/manage-memory
在 Android 2.3.3(API 級別 10)及更低版本上,位圖的後備像素數據存儲在本地內存中。它與存儲在 Dalvik 堆中的位圖本身是分開的。本地內存中的像素數據並不以可預測的方式釋放,可能會導致應用短暫超出其內存限制並崩潰。
在 Android 2.3.3(API 級別 10)及更低版本上,建議使用recycle()。如果您在應用中顯示大量位圖數據,則可能會遇到 OutOfMemoryError 錯誤。利用recycle()方法,應用可以儘快回收內存。
注意:只有當您確定位圖已不再使用時才應該使用recycle()。如果您調用recycle()並在稍後嘗試繪製位圖,則會收到錯誤:"Canvas: trying to use a recycled bitmap"。
2.Android 3.0~Android 7.1雖然3.0~7.1的版本Bitmp的像素內存是分配在java堆上的,但是實際是在natvie層進行decode的,而且會在native層創建一個c++的對象和java層的Bitmap對象進行關聯。
從BitmapFactory的源碼我們可以看到它一路調用到nativeDecodeStream這個native方法:
//BitmapFactory.javapublicstaticBitmapdecodeFile(StringpathName,Optionsopts){...stream=newFileInputStream(pathName);bm=decodeStream(stream,null,opts);...returnbm;}publicstaticBitmapdecodeStream(InputStreamis,RectoutPadding,Optionsopts){...bm=decodeStreamInternal(is,outPadding,opts);...returnbm;}privatestaticBitmapdecodeStreamInternal(InputStreamis,RectoutPadding,Optionsopts){...returnnativeDecodeStream(is,tempStorage,outPadding,opts);}nativeDecodeStream實際上會通過jni創建java堆的內存,然後讀取io流解碼圖片將像素數據存到這個java堆內存裡面:
//BitmapFactory.cppstaticjobjectnativeDecodeStream(JNIEnv*env,jobjectclazz,jobjectis,jbyteArraystorage,jobjectpadding,jobjectoptions){...bitmap=doDecode(env,bufferedStream,padding,options);...returnbitmap;}staticjobjectdoDecode(JNIEnv*env,SkStreamRewindable*stream,jobjectpadding,jobjectoptions){...//outputAllocator是像素內存的分配器,會在java堆上創建內存給像素數據,可以通過BitmapFactory.Options.inBitmap復用前一個bitmap像素內存SkBitmap::Allocator*outputAllocator=(javaBitmap!=NULL)?(SkBitmap::Allocator*)&recyclingAllocator:(SkBitmap::Allocator*)&javaAllocator;...//將內存分配器設置給解碼器decoder->setAllocator(outputAllocator);...//解碼if(decoder->decode(stream,&decodingBitmap,prefColorType,decodeMode)!=SkImageDecoder::kSuccess){returnnullObjectReturn("decoder->decodereturnedfalse");}...returnGraphicsJNI::createBitmap(env,javaAllocator.getStorageObjAndReset(),bitmapCreateFlags,ninePatchChunk,ninePatchInsets,-1);}//Graphics.cppjobjectGraphicsJNI::createBitmap(JNIEnv*env,android::Bitmap*bitmap,intbitmapCreateFlags,jbyteArrayninePatchChunk,jobjectninePatchInsets,intdensity){//java層的Bitmap對象實際上是natvie層new出來的//native層也會創建一個android::Bitmap對象與java層的Bitmap對象綁定//bitmap->javaByteArray()代碼bitmap的像素數據其實是存在java層的byte數組中jobjectobj=env->NewObject(gBitmap_class,gBitmap_constructorMethodID,reinterpret_cast<jlong>(bitmap),bitmap->javaByteArray(),bitmap->width(),bitmap->height(),density,isMutable,isPremultiplied,ninePatchChunk,ninePatchInsets);...returnobj;}我們可以看最後會調用javaAllocator.getStorageObjAndReset()創建一個android::Bitmap類型的native層Bitmap對象,然後通過jni調用java層的Bitmap構造函數去創建java層的Bitmap對象,同時將native層的Bitmap對象保存到mNativePtr:
//Bitmap.java//ConvenienceforJNIaccessprivatefinallongmNativePtr;/***Privateconstructorthatmustreceivedanalreadyallocatednativebitmap*int(pointer).*///calledfromJNIBitmap(longnativeBitmap,byte[]buffer,intwidth,intheight,intdensity,booleanisMutable,booleanrequestPremultiplied,byte[]ninePatchChunk,NinePatch.InsetStructninePatchInsets){...mNativePtr=nativeBitmap;...}從上面的源碼我們也能看出來,Bitmap的像素是存在java堆的,所以如果bitmap沒有人使用了,垃圾回收器就能自動回收這塊的內存,但是在native創建出來的nativeBitmap要怎麼回收呢?從Bitmap的源碼我們可以看到在Bitmap構造函數裡面還會創建一個BitmapFinalizer去管理nativeBitmap:
/***Privateconstructorthatmustreceivedanalreadyallocatednativebitmap*int(pointer).*///calledfromJNIBitmap(longnativeBitmap,byte[]buffer,intwidth,intheight,intdensity,booleanisMutable,booleanrequestPremultiplied,byte[]ninePatchChunk,NinePatch.InsetStructninePatchInsets){...mNativePtr=nativeBitmap;mFinalizer=newBitmapFinalizer(nativeBitmap);...}BitmapFinalizer的原理十分簡單。Bitmap對象被銷毀的時候BitmapFinalizer也會同步被銷毀,然後就可以在BitmapFinalizer.finalize()裡面銷毀native層的nativeBitmap:
privatestaticclassBitmapFinalizer{privatelongmNativeBitmap;...BitmapFinalizer(longnativeBitmap){mNativeBitmap=nativeBitmap;}...@Overridepublicvoidfinalize(){try{super.finalize();}catch(Throwablet){//Ignore}finally{setNativeAllocationByteCount(0);nativeDestructor(mNativeBitmap);mNativeBitmap=0;}}}8.0以後像素內存又被放回了native上,所以依然需要在java層的Bitmap對象回收之後同步回收native的內存。
雖然BitmapFinalizer同樣可以實現,但是Java的finalize方法實際上是不推薦使用的,所以谷歌也換了NativeAllocationRegistry去實現:
/***Privateconstructorthatmustreceivedanalreadyallocatednativebitmap*int(pointer).*///calledfromJNIBitmap(longnativeBitmap,intwidth,intheight,intdensity,booleanisMutable,booleanrequestPremultiplied,...mNativePtr=nativeBitmap;longnativeSize=NATIVE_ALLOCATION_SIZE+getAllocationByteCount();NativeAllocationRegistryregistry=newNativeAllocationRegistry(Bitmap.class.getClassLoader(),nativeGetNativeFinalizer(),nativeSize);registry.registerNativeAllocation(this,nativeBitmap);}NativeAllocationRegistry底層實際上使用了sun.misc.Cleaner,可以為對象註冊一個清理的Runnable。當對象內存被回收的時候jvm就會調用它。
importsun.misc.Cleaner;publicRunnableregisterNativeAllocation(Objectreferent,Allocatorallocator){...CleanerThunkthunk=newCleanerThunk();Cleanercleaner=Cleaner.create(referent,thunk);..}privateclassCleanerThunkimplementsRunnable{...publicvoidrun(){if(nativePtr!=0){applyFreeFunction(freeFunction,nativePtr);}registerNativeFree(size);}...}這個Cleaner的原理也很暴力,首先它是一個虛引用,registerNativeAllocation實際上創建了一個Bitmap的虛引用:
//Cleaner.javapublicclassCleanerextendsPhantomReference{...publicstaticCleanercreate(Objectob,Runnablethunk){...returnadd(newCleaner(ob,thunk));}...privateCleaner(Objectreferent,Runnablethunk){super(referent,dummyQueue);this.thunk=thunk;}...publicvoidclean(){...thunk.run();...}...}虛引用的話我們都知道需要配合一個ReferenceQueue使用,當對象的引用被回收的時候,jvm就會將這個虛引用丟到ReferenceQueue裡面。而ReferenceQueue在插入的時候居然通過instanceof判斷了下是不是Cleaner:
//ReferenceQueue.javaprivatebooleanenqueueLocked(Reference<?extendsT>r){...if(rinstanceofCleaner){Cleanercl=(sun.misc.Cleaner)r;cl.clean();...}...}也就是說Bitmap對象被回收,就會觸發Cleaner這個虛引用被丟入ReferenceQueue,而ReferenceQueue裡面會判斷丟進來的虛引用是不是Cleaner,如果是就調用Cleaner.clean()方法。而clean方法內部就會再去執行我們註冊的清理的Runnable。
近期活動安排:
6月9日深圳活動報名 | 幫助企業玩轉出海,就在 Google Solution Day
6月18日廣州活動報名 | 廣州 Google I/O Extended 2022 揚帆起航
6月18日廣州 GDG DevOps Codelab開放報名中 | Be Pioneer!花樣玩轉極狐GitLab
6月19日東莞First,2022第一場東莞線下活動-Google I/O Extended
Google Developer Groups,簡稱GDG,中文名為谷歌開發者社區,是谷歌開發者部門發起的全球項目,面向對Google和開源技術感興趣的人群而存在的公益性開發者社區。GDG廣州創立於2009年,是全球GDG社區中最活躍和知名的技術社區之一,每年舉辦數十場大大小小的科技活動,每年影響十幾萬以廣深莞為中心,輻射粵港澳大灣區的開發者及科技從業人員。
社區中的各位組織者均是來自各個行業有着本職工作的互聯網從業者,我們需要更多新鮮血液的加入!如果你對谷歌技術感興趣,業餘時間可調配,認同社區的價值觀,願意為社區做出貢獻,歡迎加入我們成為社區志願者!如果你能為活動提供餐飲、物料製作、禮品、宣發、會務等支持,歡迎聯繫我們成為贊助合作夥伴。
有意願成為GDG社區志願者、贊助商、講師及社區合作夥伴的朋友,請聯繫wechatID:lyyahyc
社區成員加入方式:
1.關注本公眾號:GDG廣州
2.社區成員可以通過郵箱接收到我們的活動信息,請發任意郵件至以下郵箱,發送空郵件到guangzhou-gtug+subscribe@googlegroups.com即完成訂閱