如何改善Bitmap所帶來的Out of Memory(OOM)問題

如何改善Bitmap所帶來的Out of Memory(OOM)問題

我們很常使用Bitmap來存放圖片,但是常常因為檔案太大,
一不小心就跳出Out of Memory(OOM)的訊息,
為什麼會產生OOM的問題呢?
原因就出在Android的限制, Android是跑在手機上的作業系統,
因此我們手機上的記憶體就不可能像PC上面的記憶體一樣,
想擴增就擴增, 因此每一點一滴的記憶體我們都必須斤斤計較,
而Android的限制就是當你超過16MB的檔案, 它就會跳出OOM的警告視窗,
警告你不能使用這麼大的檔案。

從這個角度來想, 我們就有幾個思考的方向,

1. 從圖檔著手。
2. 從程式碼著手。

第一點沒什麼好說的, 如果你想要下載大圖到手機上或者平板上,
通常是沒什麼必要的, 畢竟人的眼睛對於解析度高跟解析度普通的圖片,
看不出太大的差別, 所以建議最好不要找那麼大的解析度。

所以我們就單純討論第二點,
引述這裡的方法
http://www.ptt.cc/bbs/AndroidDev/M.1308308761.A.77E.html

解決方式大概有以下幾種:

  1. 使用 BitmapFactory.decodeStream() 產生Bitmap
  2. 設定 BitmapFactory.Options inSampleSize,
         (inSampleSize值越大解析度越小,佔用memory也越小)
  3. 在 onPause/onStop/onDestory 時,將沒用到的Bitmap release(用recyle())

根據第一點
我們可以這樣寫,

public Bitmap getLocalBitmap(Context con, int resourceId){
    InputStream inputStream = con.getResources().openRawResource(resourceId);
    return BitmapFactory.decodeStream(inputStream, null, getBitmapOptions());
}

當你傳入一個Resource id的時候, 利用context的方法把它轉成串流, 就可以使用BitmapFactory.decodeStream這個方法, 為什麼要使用這個方法來改善呢?
根據這篇文章
http://shukaiyang.myweb.hinet.net/notes/mobile/android_oom.htm
裡面所敘述的
decodeStream最大的秘密在於其直接調用JNI>>nativeDecodeAsset()來完成decode,無需再使用java層的createBitmap,從而節省了java層的空間。

因此我們用這樣的方法來讀取專案內的圖檔應該是最恰當的。

接著第二點
如果一開始就把圖片設定為小圖片, 那麼想當然圖片容量就會變小,
我們可以這樣寫

public BitmapFactory.Options getBitmapOptions(int scale){
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPurgeable = true;
    options.inInputShareable = true;
    options.inSampleSize = scale;
    return options;
}

這邊稍微說明一下
根據這裡的敘述
http://pernghh.pixnet.net/blog/post/35164141-%E8%A7%A3%E6%B1%BA%E5%9C%A8-android-%E4%B8%AD%E4%BD%BF%E7%94%A8-bitmap-%E9%80%A0%E6%88%90-out-of-memory-%E7%9A%84

inPurgeable 設定為 true,這樣可以讓java系統記憶體不足時先行回收部分的記憶體
而根據官網的敘述
http://developer.android.com/reference/android/graphics/BitmapFactory.Options.html

public boolean inInputShareable : This field works in conjuction with inPurgeable.

因此我們就將這兩個參數都設定為true

最後是我們inSimpleSize的大小由程式撰寫者來設定,
我通常看圖片大小大概設2~4就差不多了。

那麼第三點的討論
也就是說在程式中的各種狀態, 隨時就來個

bitmap.recycle();
System.gc();

通知一下系統, 該回收了喔!

其實我還找到一些方法可以稍微改善一下記憶體的使用程度,
例如
在BitmapFactory.Options裏竟然有一個inNativeAlloc的public變數,可以直接不把使用的記憶體算到VM裏,有趣的是這個變數是個隱藏版的變數,所以在正常的SDK文件中看不到,用eclipse時也不會提示你,也不能直接用,因此我用了一些小技巧將這個變數設成true,如此一來bitmap out of memory的問題發生的機率又更低了

寫法大概像這樣

BitmapFactory.Options.class.getField("inNativeAlloc").setBoolean(options,true);

還有

Android堆記憶體也可自己定義大小

對於一些Android專案,影響性能瓶頸的主要是Android自己記憶體管理機制問題,目前手機廠商對RAM都比較吝嗇,對於軟體的流暢性來說RAM對性能的影響十分敏感,除了 優化Dalvik虛擬機的堆記憶體分配外,我們還可以強制定義自己軟體的對記憶體大小,我們使用Dalvik提供的 dalvik.system.VMRuntime類來設置最小堆記憶體為例:

寫法像這樣

private final static int CWJ_HEAP_SIZE = 6* 1024* 1024 ;
VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);

還有雖然前面的連結有說過, 非必要的時候, 盡量不要使用BitmapFactory.createBitmap來建立
一張bitmap, 但是其實createBitmap也是可以優化的。
http://topic.csdn.net/u/20100208/15/acfacf0b-654e-4f8e-82c3-a240277fa2b6.html

Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8);
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);

這四種分別代表著不同的意義

Bitmap.Config ALPHA_8   
Bitmap.Config ARGB_4444   
Bitmap.Config ARGB_8888   
Bitmap.Config RGB_565  
A R G B

透明度 红色 绿色 蓝色

Bitmap.Config ARGB_4444 每個pixel 佔四位   
Bitmap.Config ARGB_8888 32 每個pixel 佔八位  
Bitmap.Config RGB_565 16 R佔5位 G佔6位 B占5位 没有透明度(A)

其實很簡單 就跟double float一樣 你解析度越高圖檔就越大 無可厚非
一般情況下我們都是用ARGB_8888 但是相對它也佔相當大的記憶體
因為一個pixel 32bit 8個bit=1word 如果是800*400的圖片 則會變成1MB多