一個Bitmap使用完后,是只需要等它成為垃圾后讓GC去回收,還是應該主動調用recycle方法呢?或者說,主動調用recycle方法是否有好處,是否能馬上回收內存呢?
帶着這個問題來看源碼(我看的4.4源碼)。
先看Bitmap內存的創建,通過跟蹤Bitmap.createBitmap方法,可以發現是native方法里調用的JVM來創建的:
jbyteArray arrayObj = env->NewByteArray(size);
native使用的是通過其得到的一個固定地址:
jbyte* addr = jniGetNonMovableArrayElements(&env->functions, arrayObj);
native里會用一個SkPixelRef來存放他們:
SkPixelRef* pr = new AndroidPixelRef(env, bitmapInfo, (void*) addr, bitmap->rowBytes(), arrayObj, ctable);
然后將這個傳給Bitmap:
bitmap->setPixelRef(pr);
再看recycle過程:
java端:
mBuffer = null;
native端:
Caches::getInstance().textureCache.removeDeferred(bitmap);
fPixelRef->unref(); // fPixelRef是上面分配的SkPixelRef
fPixelRef = NULL;
這里其實就是java端將mBuffer置為垃圾。native端釋放SkPixelRef,並延遲刪除其對應的TextureCache(最終的刪除應該是在下一幀開始前)。
再看Bitmap的finalize,發現Bitmap類自己沒有finalize,專門用了一個靜態內部類BitmapFinalizer,其finalize方法來做native資源的釋放。至於為什么要這么弄,我后面另說。
其會調用到native里:
Caches::getInstance().textureCache.removeDeferred(resource);
delete resource;
延遲刪除其對應的TextureCache,並刪除SkBitmap。
從這么來看,recycle方法會釋放部分native內存,但並不會釋放Bitmap占用內存最大的圖像數據內存。
但我突然想到,好像截屏時得到的Bitmap的圖像數據內存並不是在JVM里申請的,查看代碼,果然是這樣。其並不是通過Bitmap.createBitmap方法創建的圖像:
GraphicsJNI::createBitmap(env, bitmap, GraphicsJNI::kBitmapCreateFlag_Premultiplied, NULL);
其使用的SkPixelRef也不是上面的AndroidPixelRef,而是ScreenshotPixelRef,里面持有着圖像數據。
在這種情況下,調用recycle方法是會釋放其圖像數據的。
另外要命的是,假如我們不停截屏並丟掉之前的Bitmap,我們可能覺得很容易就會有垃圾回收,那么之前的Bitmap就回收了。可是由於Bitmap的圖像數據才是內存大戶,Bitmap本身占用內存非常小,因此這種情況下Bitmap的構造引起垃圾回收的可能性很低。
我做了個試驗,在app里點擊按鈕截一次屏,然后馬上扔掉,使其為垃圾。我不停點擊按鈕,最終系統內存耗盡導致app被殺,也沒有發生GC。改為每次截屏后手動調用gc,就不會導致內存增大。
並且危險的是,這部分內存並不是分配在app端,就算app被殺也不會釋放。(截屏的內存是在SurfaceFlinger端申請的,app端的釋放應該只是把內存使用權還給SurfaceFlinger,SurfaceFlinger會繼續重用它,但不會徹底釋放還給系統,因此變大之后不會變小,不清楚有沒有最終的釋放邏輯。我以前在做系統截屏的時候曾因為沒有主動recycle導致占用極大系統內存。)
畫個表格來說明一下recycle在各種情況下會回收哪些內存吧:
|
SkPixelRef (小) |
SkBitmap (小) |
圖像數據 (面積大則很大) |
TextureCache (同圖像數據相當) |
---|---|---|---|---|
JVM中分配圖像數據如Bitmap.createBitmap 且沒有被硬件加速draw過 |
√ | × | × | 無此內存 |
JVM中分配圖像數據如Bitmap.createBitmap 且有被硬件加速draw過 |
√ | × | × | √ |
不在JVM中分配圖像數據如截屏 |
√ | × | √ | 情況同上 |
這些內存在其Bitmap成為垃圾后的垃圾回收過程里都會釋放。但是,在native內存吃緊的情況下系統是不知道可以通過GC來回收一部分native內存的,所以盡早釋放是有積極作用的。
結論:盡快的調用recycle是個好習慣,會釋放與其相關的native分配的內存;但一般情況下其圖像數據是在JVM里分配的,調用recycle並不會釋放這部分內存。
我們用createBitmap創建的Bitmap且沒有被硬件加速Canvas draw過,則主動調用recycle產生的意義比較小,僅釋放了native里的SkPixelRef的內存,這種情況我覺得可以不主動調用recycle。
被硬件加速Canvas draw過的由於有TextureCache應該盡快調用recycle來盡早釋放其TextureCache。
像截屏這種不是在JVM里分配內存的情況也應該盡快調用recycle來馬上釋放其圖像數據。
(一個例外,如果是通過Resources.getDrawable得到的Bitmap,不應該調用recycle,因為它可能會被重用)
另,說一下上面提到的Bitmap的finalize實現方式。
這里沒有直接在Bitmap上實現finalize,而是用一個靜態內部類專門實現finalize,這是因為在GC過程中,沒有finalize的對象可以直接回收,而有finalize的對象需要多保持一會兒來執行其finalize方法,然后才能回收,在我以前了解的C#的垃圾回收機制里,有finalize的對象在第一次GC的時候只會執行其finalize方法,要到下一次GC才會回收其內存。所以Bitmap的這種finalize實現方式是為了讓占用內存大的部分(Bitmap類)沒有finalize,可以早點釋放;BitmapFinalizer內部類僅持有一個NativeBitmap指針,通過finalize去釋放native內存。這樣最有效的達到既提前釋放主要內存又能通過finalize釋放native內存的目的。