本文來自網易雲社區。

最近分析線上日誌,發現存在一定量的OutOfMemoryError。由於Android系統對堆內存大小作了限制,不同的設備上這個閾值也會不同,當已分配內存加新分配內存大於堆內存就會導致OOM。雖然Android機型的配置在不斷升級,但還是存在著幾年前的舊機型,它們的特點是內存小,尤其在涉及大圖片載入時很容易出現OOM。

概述

為了避免OOM,程序應該增加可用內存,並及時回收不再使用的對象,降低內存佔用。可以從以下幾個方面去考慮:

1、對圖片進行處理,如圖片裁剪和壓縮。

使用縮略圖來提高載入速度和降低內存佔用。根據控制項大小對圖片進行裁剪,減少不必要內存浪費。我們的項目中使用了NOS提供的圖片處理服務,它提供了非常強大的雲處理功能,在開發過程中根據實際需要生成請求鏈接,獲取不同尺寸的圖片,實現圖片裁剪。同時使用BitmapFactory.Options屬性,通過設置採樣率, 減少Bitmap的像素。

2、內存引用上做一些處理,常用的有軟引用。

使用軟引用的對象在內存足夠時,垃圾回收器不會回收它;當內存空間不足時,為滿足程序運行的需求,會回收這些對象,避免出現OOM導致的程序崩潰。因此只要對象沒有被回收都能被程序使用。 過去很多應用都大量使用軟引用進行圖片緩存,通過GC自動回收圖片所佔內存。從Android 2. 3開始,垃圾回收器會更傾向於回收持有軟引用或弱引用的對象,這讓軟引用和弱引用變得不再可靠。另外,在Android 3.0中,圖片的數據會存儲在本地的內存當中,因而無法用一種可預見的方式將其釋放,這就有潛在的風險造成應用程序的內存溢出並崩潰,所以在開發過程中要謹慎使用。

3、緩存機制。

緩存不僅可以減少流量的浪費還能防止載入過多的圖片,項目中使用了比較主流的內存、文件和網路三級緩存。

通過URL向網路請求圖片時,先從內存中查找,如果內存中沒有,再從緩存文件中查找,如果緩存文件中也沒有,再向網路發Http請求下載圖片,然後再依次緩存在內存和文件中。

在項目中使用了強引用(LRUCache)與軟引用相結合的方式進行內存緩存。系統不會回收強引用的對象,為了防止OOM,需要為LRUCache設置適當的大小,並及時回收內存。因為堆空間又被分為年輕代、老年代和永久代,新分配的對象會先放在年輕代中,當停留一段時間後,這個對象會被移動到老年代,最後再移動到永久代中。系統的每一個內存區域都採用不同的策略進行GC操作,年輕代的對象更容易被銷毀,而且GC操作的速度比老年代的速度要快,時間更短。

同時Android系統並不會對空閑內存區域做碎片整理,只有在內存不足時觸發GC進行回收,從而造成空間上的浪費。因此,程序應該在適當的時候主動回收不再使用的圖片,減少被動回收導致的內存溢出風險。

4、自定義堆內存大小,如使用largeHeap。

在Manifest.xml中的Application節點下加入android:largeHeap="true",系統便能為應用程序分配更多的內存空間,但是這種方式不能根本解決問題,不合理的使用內存同樣會造成OOM,只是延緩其發生。對於一些內存佔用比較大的圖片、視頻類應用,最好在開發測試過後再加上該屬性。

5、使用第三方開源圖片框架,比如Picasso、Glide、Fresco等,它們在圖片非同步載入、緩存、內存管理和優化等方面已經做了很好的處理。

基於LRUCache的緩存

前面介紹了幾種避免OOM的方式,在實際項目中需要結合使用。本文主要介紹內存緩存的實現,包括強引用緩存和軟引用緩存兩個部分。強引用緩存採用LRUCache實現,它是Android系統為開發人員提供的緩存工具類,實際上是將強引用的對象存儲在LinkedHashMap中,初始化時會設置緩存空間大小,當緩存數據達到預設值時會採用最近最少使用演算法進行淘汰。另外,軟引用緩存同樣使用LinkedHashMap作為存儲結構,將從LRUCache淘汰的數據扔到軟引用緩存中,之前的做法是對軟引用對象不做任何處理,等待垃圾回收器自動回收。大量使用軟引用的弊端前面也有介紹,本文對此做了部分改進,有限的使用軟引用對象,當軟引用緩存空間不足時,同樣按照LRU規則淘汰並主動回收內存空間。

首先,通過圖片的URL從網路下載圖片,將圖片先緩存到內存緩存中,緩存到強引用也就是LruCache中。如果LruCache空間不足,就會將較早存儲的圖片對象淘汰到軟引用緩存中,然後將圖片緩存到文件中。在讀取圖片時,先讀取內存緩存,判斷LruCache是否存在圖片,如果存在,則直接讀取,如果LruCache中不存在,則判斷軟引用中是否存在,如果軟引用中存在,則將軟引用中的圖片添加到LruCache並且刪除軟引用中的數據,如果軟引用中不存在,則從文件或網路讀取。

代碼分析與實現

首先,通過繼承LRUCache類實現BitmapLRUCache,裡面的鍵值對分別是URL和對應圖片的Drawable對象

class BitmapLRUCache extends LruCache<String, Drawable>

然後在構造方法中初始化軟引用緩存mSoftBitmapCache,並設置LRUCache的大小,這裡設置為手機可用內存的1/4。

int maxMemory=(int)Runtime.getRuntime().maxMemory()/4;

通過LRUCache構造方法的源碼可以看出,實際上是初始化一個LinkedHashMap,並且LinkedHashMap中的對象採用LRU規則自動排序。

public LruCache(int maxSize) {
... ...
this.maxSize = maxSize;
this.map = new LinkedHashMap(0, 0.75f, true);
}
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
init();
this.accessOrder = accessOrder;
}

在LruCache中初始化LinkedHashMap構造方法的accessOrder參數值為true,這個參數默認為false,表示對象按照插入順序排序。下面是LruCache類的get方法:

public V get(Object key) {
... ...
for (HashMapEntry e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
if (accessOrder)
makeTail((LinkedEntry) e);
return e.value;
}
}
return null;
}

當向緩存get數據時,如果accessOrder為true,則通過makeTail((LinkedEntry) e)方法將對象移到了末尾, 這樣就能夠保證每次從頭部移除最近最少使用的對象。

如果向LRUCache中插入圖片對象,當緩存空間不足時,需要移除最近最少使用對象,由於LinkedHashMap已經做好了排序, 所以直接移除頭部對象即可。

下面是LRUCache的put方法:

public final V put(K key, V value) {
... ... V previous;
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}

其中safeSizeOf(key, value)用來獲取待插入對象的大小,並對已佔用內存進行累加。再看看這個方法:

private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}

返回的result通過sizeOf(key, value)這個方法獲取,到這裡我們明白了需要重寫sizeOf方法, 這裡用每行像素點所佔用的位元組數乘高度計算出圖片大小。

protected int sizeOf(String key, Drawable value) {
if(value!=null) {
if (value instanceof BitmapDrawable) {
Bitmap bitmap = ((BitmapDrawable) value).getBitmap();
return bitmap.getRowBytes() * bitmap.getHeight();
}
return 1;
}else{
return 0;
}
}

然而真正移除對象是在trimToSize(maxSize)這個方法中:

public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
... ...
if (size <= maxSize || map.isEmpty()) {
break;
}
Map.Entry toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}

這裡會檢查當前緩存容量,size <= maxSize便移除頭部對象。最後調用了entryRemoved(true, key, value, null)方法。

因此,我們可以重寫該方法來處理淘汰對象:

protected void entryRemoved(boolean evicted, String key, Drawable oldValue, Drawable newValue) {
if (evicted) {
if (oldValue != null) {
//當硬緩存滿了,根據LRU規則移入軟緩存
synchronized(mSoftBitmapCache) {
mSoftBitmapCache.put(key, new SoftReference(oldValue));
}
}
}else{//主動移除,回收無效空間
recycleDrawable(oldValue);
}
}

當evicted變數為true時,屬於為騰出緩存空間被調用,將被淘汰的對象插入軟引用緩存mSoftBitmapCache中。

當evicted變數為false時,屬於主動淘汰對象,看下面代碼:

public final V remove(K key) {
... ...
V previous;
synchronized (this) {
previous = map.remove(key);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}

entryRemoved方法在LRUCache的remove方法中調用時,evicted參數的值為false,因此這裡直接回收圖片對象。

如果軟引用緩存mSoftBitmapCache超出上限,也根據LRU規則進行淘汰,直接回收對象的內存空間。這裡參考LRUCache的實現方式進行初始化:

this.mSoftBitmapCache= new LinkedHashMap>(SOFT_CACHE_SIZE, 0.75f, true){
@Override
protected boolean removeEldestEntry(Entry> eldest) {
if (size() > SOFT_CACHE_SIZE) {//緩存數量不超過10
if(eldest!=null){
SoftReference bitmapReference=eldest.getValue();
if(bitmapReference!=null){
Drawable oldValue=bitmapReference.get();
recycleDrawable(oldValue);
}
}
return true;
}
return false;
}
};

不同的是重寫了removeEldestEntry方法,這個方法主要用於判斷緩存容量是否超過上限,如果超出則回收被淘汰的對象。

再看看LinkedHashMap類的put方法調用了addNewEntry方法,在該方法中會根據removeEldestEntry方法的返回來決定是否移除對象:

public V put(K key, V value) {
... ...
addNewEntry(key, value, hash, index);
return null;
}
void addNewEntry(K key, V value, int hash, int index) {
LinkedEntry header = this.header;
// Remove eldest entry if instructed to do so.
LinkedEntry eldest = header.nxt;
if (eldest != header && removeEldestEntry(eldest)) {
remove(eldest.key);
}
......
}

因此,當size() > SOFT_CACHE_SIZE時,便對老對象進行移除操作。 從緩存中獲取對象的方法:

public Drawable getBitmap(String url){
// 先從硬緩存中獲取
Drawable bitmap = get(url);
if (bitmap != null) {
return bitmap;
}
synchronized (mSoftBitmapCache) {
SoftReference bitmapReference = mSoftBitmapCache.get(url);
if (bitmapReference != null) {
bitmap = bitmapReference.get();
if (bitmap != null) {
//移入硬緩存
put(url, bitmap);
mSoftBitmapCache.remove(url);
return bitmap;
} else {
mSoftBitmapCache.remove(url);
}
}
}
return null;
}

優先從硬緩存中拿,如果存在則返回。否則查詢軟引用緩存,存在則返回對象並移入硬緩存中。

最後上完整的代碼:

public class BitmapLRUCache extends LruCache {
private final int SOFT_CACHE_SIZE = 10; // 軟引用緩存容量
private LinkedHashMap> mSoftBitmapCache;//軟引用緩存,已清理的數據可能會再次使用
public BitmapLRUCache(int maxSize) {
super(maxSize);
this.mSoftBitmapCache= new LinkedHashMap>(SOFT_CACHE_SIZE, 0.75f, true){// true 採用LRU排序,移除隊首
@Override
protected boolean removeEldestEntry(Entry> eldest) {
if (size() > SOFT_CACHE_SIZE) {//緩存數量不超過10
if(eldest!=null){
SoftReference bitmapReference=eldest.getValue();
if(bitmapReference!=null){
Drawable oldValue=bitmapReference.get();
recycleDrawable(oldValue);
}
}
return true;
}
return false;
}
};
}
public Drawable getBitmap(String url){
// 先從硬緩存中獲取
Drawable bitmap = get(url);
if (bitmap != null) {
return bitmap;
}
synchronized (mSoftBitmapCache) {
SoftReference bitmapReference = mSoftBitmapCache.get(url);
if (bitmapReference != null) {
bitmap = bitmapReference.get();
if (bitmap != null) {
//移入硬緩存
put(url, bitmap);
mSoftBitmapCache.remove(url);
return bitmap;
} else {
mSoftBitmapCache.remove(url);
}
}
}
return null;
}
private int getSizeInBytes(Bitmap bitmap) {
int size = bitmap.getRowBytes() * bitmap.getHeight();//每一行像素點所佔用的位元組數 * 高度
return size;
}
protected int sizeOf(String key, Drawable value) {
if(value!=null) {
if (value instanceof BitmapDrawable) {
Bitmap bitmap = ((BitmapDrawable) value).getBitmap();
return getSizeInBytes(bitmap);
}
return 1;
}else{
return 0;
}
}
protected void entryRemoved(boolean evicted, String key, Drawable oldValue, Drawable newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
if (evicted) {
if (oldValue != null) {
//當硬緩存滿了,根據LRU規則移入軟緩存
synchronized(mSoftBitmapCache) {
mSoftBitmapCache.put(key, new SoftReference(oldValue));
}
}
}else{//主動移除,回收無效空間
recycleDrawable(oldValue);
}
}
private void recycleDrawable(Drawable oldValue) {
if (oldValue != null) {
try {
if (oldValue instanceof BitmapDrawable) {
Bitmap bitmap = ((BitmapDrawable) oldValue).getBitmap();
bitmap.recycle();
}
Log.i("BitmapLRUCache", "oldValue:" + oldValue);
} catch (Exception exception) {
Log.i("BitmapLRUCache", "Failed to clear Bitmap images on close", exception);
} finally {
oldValue = null;
}
}
}
}

測試

測試機器為華為G525,系統版本為4.1。運行改進後的代碼,在AndroidStudio中查看Monitors欄,啟動程序並進行簡單操作,很清楚的看到內存佔用的實時變化以及釋放的過程。

改進前內存保持在30M到40M之間,並且通過log日誌觀察GC暫停時間相對較長。改進後內存保持在20M以下。測試結果,有效的降低了內存佔用。

總結

應用程序過高的內存佔用,資源不能及時釋放,容易導致OOM。但內存佔用也不是越少就越好,如果為了保持較低的內存佔用而頻繁觸發GC操作,可能會造成程序性能的整體下降。因此,需要在實踐中進行綜合考慮做一定的權衡。 參考資料

jianshu.com/p/f5d8d3066

my.oschina.net/u/586684bozhiyue.com/anroid/bokblog.chinaunix.net/uid-developer.android.com/i

原文:Android圖片緩存分析與優化,經作者潘威授權發布

了解網易雲 :

網易雲官網:https://www.163yun.com

網易雲社區:sq.163yun.com/blog

更多網易研發、產品、運營經驗分享請訪問網易雲社區。

推薦閱讀:

相关文章