該編文章轉自:
http://7dot9.com/2011/08/android%e4%b8%ad%e8%a7%a3%e5%86%b3%e5%9b%be%e5%83%8f%e8%a7%a3%e7%a0%81%e5%af%bc%e8%87%b4%e7%9a%84oom%e9%97%ae%e9%a2%98/
在上一篇博文Android Bitmap記憶體限制中我們詳細的了解并分析了Android為什麼會在Decode Bitmap的時候出現OOM錯誤,簡單的講就是Android在解碼圖檔的時候使用了本地代碼來完成解碼的操作,但是使用的記憶體是堆裡面的記憶體,而堆記憶體的大小是收VM執行個體可用記憶體大小的限制的,是以當應用程式可用記憶體已經無法再滿足解碼的需要時,Android将抛出OOM錯誤。
這裡講一個題外話,也就是為何Android要限制每個應用程式的可用記憶體大小呢?其實這個問題可能有多方面的解答,目前我自己考慮到的有兩點:
- 使得記憶體的使用更為合理,限制每個應用的可用記憶體上限,可以防止某些應用程式惡意或者無意使用過多的記憶體,而導緻其他應用無法正常運作,我們衆所周知的Android是有多程序的,如果一個程序(也就是一個應用)耗費過多的記憶體,其他的應用還搞毛呢?當然在這裡其實是有一個例外,那就是如果你的應用使用了很多本地代碼,在本地代碼中建立對象解碼圖像是不會被計算到的,這是因為你使用本地方法建立的對象或者解碼的圖像使用的是本地堆的記憶體,跟系統是平級的,而我們通過Framework調用BitmapFactory.decodeFile()方法解碼時,系統雖然也是調用本地代碼來進行解碼的,但是Android Framework在實作的時候,刻意地将這部分解碼使用的記憶體從堆裡面配置設定了而不是從本地堆裡配置設定的記憶體,是以才會出現OOM,當然并不是說從本地堆裡配置設定就不會出現OOM,本地堆配置設定記憶體超過系統可用記憶體限制的話,通常都是直接崩潰,什麼錯誤可能都看不到,也許會有一些崩潰的錯誤位元組碼之類的。
- 省電的考慮,呃…,原因我好像也不能很明白地說出來。
回到正題來,我們在應用的設計和開發中可能會經常碰到需要在一個界面上顯示數十張圖檔乃至上百張,當然限于手機螢幕的大小我們通常在設計中會使用類似于清單或者網格的控件來展示,也就是說通常一次需要顯示出來圖檔數還是一個相對确定的數字,通常也不會太大。如果數目比較大的畫,通常顯示的控件自身尺寸就會比較小,這個時候可以采用縮略圖政策。下面我們來看看如果避免出現OOM的錯誤,這個解決方案參考了Android示範程式XML Adapters中的ImageDownloader.java中的實作,主要是使用了一個二級緩存類似的機制,就是有一個資料結構中直接持有解碼成功的Bitmap對象引用,同時使用一個二級緩存資料結構持有解碼成功的Bitmap對象的SoftReference對象,由于SoftReference對象的特殊性,系統會在需要記憶體的時候首先将SoftReference對象持有的對象釋放掉,也就是說當VM發現可用記憶體比較少了需要觸發GC的時候,就會優先将二級緩存中的Bitmap回收,而保有一級緩存中的Bitmap對象用于顯示。
其實這個解決方案最為關鍵的一點是使用了一個比較合适的資料結構,那就是LinkedHashMap類型來進行一級緩存Bitmap的容器,由于LinkedHashMap的特殊性,我們可以控制其内部存儲對象的個數并且将不再使用的對象從容器中移除,這就給二級緩存提供了可能性,我們可以在一級緩存中一直儲存最近被通路到的Bitmap對象,而已經被通路過的圖檔在LinkedHashMap的容量超過我們預設值時将會把容器中存在時間最長的對象移除,這個時候我們可以将被移除出LinkedHashMap中的對象存放至二級緩存容器中,而二級緩存中對象的管理就交給系統來做了,當系統需要GC時就會首先回收二級緩存容器中的Bitmap對象了。在擷取對象的時候先從一級緩存容器中查找,如果有對應對象并可用直接傳回,如果沒有的話從二級緩存中查找對應的SoftReference對象,判斷SoftReference對象持有的Bitmap是否可用,可用直接傳回,否則傳回空。
主要的代碼段如下:
private static final int HARD_CACHE_CAPACITY = 16;
// Hard cache, with a fixed maximum capacity and a life duration
private static final HashMap<String, Bitmap> sHardBitmapCache = new LinkedHashMap<String, Bitmap>(HARD_CACHE_CAPACITY / 2, 0.75f, true) {
private static final long serialVersionUID = -57738079457331894L;
@Override
protected boolean removeEldestEntry(LinkedHashMap.Entry<String, Bitmap> eldest) {
if (size() > HARD_CACHE_CAPACITY) {
// Entries push-out of hard reference cache are transferred to soft reference cache
sSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue()));
return true;
} else
return false;
}
};
// Soft cache for bitmap kicked out of hard cache
private final static ConcurrentHashMap<String, SoftReference<Bitmap>> sSoftBitmapCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>(HARD_CACHE_CAPACITY / 2);
public Bitmap getBitmap(String id) {
// First try the hard reference cache
synchronized (sHardBitmapCache) {
final Bitmap bitmap = sHardBitmapCache.get(id);
if (bitmap != null) {
// Bitmap found in hard cache
// Move element to first position, so that it is removed last
sHardBitmapCache.remove(id);
sHardBitmapCache.put(id, bitmap);
return bitmap;
}
}
// Then try the soft reference cache
SoftReference<Bitmap> bitmapReference = sSoftBitmapCache.get(id);
if (bitmapReference != null) {
final Bitmap bitmap = bitmapReference.get();
if (bitmap != null) {
// Bitmap found in soft cache
return bitmap;
} else {
// Soft reference has been Garbage Collected
sSoftBitmapCache.remove(id);
}
}
return null;
}
public void putBitmap(String id, Bitmap bitmap) {
synchronized (sHardBitmapCache) {
if (sHardBitmapCache != null) {
sHardBitmapCache.put(id, bitmap);
}
}
}
上面這段代碼中使用了id來辨別一個Bitmap對象,這個可能大家在實際的應用中可以選擇不同的方式來索引Bitmap對象,圖像的解碼在這裡就不做贅述了。這裡主要讨論的就是如何管理Bitmap對象,使得在實際應用中不要輕易出現OOM錯誤,其實在這個解決方案中,HARD_CACHE_CAPACITY的值就是一個經驗值,而且這個跟每個應用中需要解碼的圖檔的實際大小直接相關,如果圖檔偏大的話可能這個值還得調小,如果圖檔本身比較小的話可以适當的調大一些。本解決方案主要讨論的是一種雙緩存結合使用SoftReference的機制,通過使用二級緩存和系統對SoftReference對象的回收特性,讓系統自動回收不再敏感的圖檔Bitmap對象,而保有一級緩存也就是敏感的圖檔Bitmap對象。