在上一篇博文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);
/**
* @param id
* The ID of the image that will be retrieved from the cache.
* @return The cached bitmap or null if it was not found.
*/
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對象。