Picasso這個圖片框架默認實現了內存中的LRU緩存,但是沒有默認實現磁盤緩存(關於磁盤緩存的配置可以看我之前寫的一篇博客),我在使用Picasso替換原來的xUtils框架的時候發現內存開銷要比之前高好多,於是著手分析Picasso的LRU緩存策略,代碼比較好讀,下面簡單的分析一下。
Picasso加載一個圖片的流程一般是這樣的:
url->檢查LRU緩存中有沒有對應的bitmap->調用HTTP框架准備下載該圖片資源->http框架檢查有沒有磁盤緩存->http框架訪問網絡下載數據並進行緩存
這裡面的動作主要是由一個叫BitmapHunter的類完成的。
Picasso有一個接口叫Cache,有一個實現叫LruCache,這個實現類裡面是用一個LinkedHashMap<String, Bitmap>來進行緩存,key是圖片url,value是bitmap,並不是其他框架愛用的WeakReference方案。
這個實現類裡面有幾個控制內存使用量的成員,如下:
private final int maxSize;//最大堆內存占用,單位字節
private int size;//當前已經緩存到堆內存中所有bitmap所占的字節數
private int putCount;//將bitmap存入LRU緩存的總次數
private int evictionCount;//因為內存不足而將bitmap移出LRU緩存的總次數
private int hitCount;//從LRU緩存中讀取bitmap的總次數
private int missCount;//沒有從LRU緩存中根據url找到相應的bitmap的總次數
來看一下添加一個bitmap到緩存的代碼
@Override public void set(String key, Bitmap bitmap) {
if (key == null || bitmap == null) {
throw new NullPointerException("key == null || bitmap == null");
}
Bitmap previous;
synchronized (this) {//每次只能讀寫一個bitmap,因為LinkedHashMap是非線程安全的
putCount++;//存bitmap計數器加一
size += Utils.getBitmapBytes(bitmap);//獲取一個bitmap所占內存的字節數
previous = map.put(key, bitmap);//將bitmap存入到hashmap中去,以url為key,如果previous為空說明之前沒有存儲過該url,否則之前存儲過
if (previous != null) {//如果之前已經存儲過這個url了
size -= Utils.getBitmapBytes(previous);
}
}
<span > </span>
trimToSize(maxSize);//看看內存占用是否過大,如果太大的話就從LRU緩存中移出一部分bitmap
}
最重要的方法就是這個trimToSize(),它是用來回收bitmap緩存的,讓我們來著重研究一下
private void trimToSize(int maxSize) {
while (true) {//一直執行銷毀動作,直到當前占用的內存字節數小於規定的最大占用量
String key;
Bitmap value;
synchronized (this) {//由於LinkedHashMap線程非安全,並且只有逐個釋放才能准確比較剩余LRU大小,所以要同步執行
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(
getClass().getName() + ".sizeOf() is reporting inconsistent results!");
}
if (size <= maxSize || map.isEmpty()) {
break;
}
<span > </span>//LinkedHashMap可以看作是一個先入先出的棧,回收內存的時候先從棧底開始回收,也就是回收好久沒用過的bitmap
Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);//將bitmap移出LRU緩存
size -= Utils.getBitmapBytes(value);//將當前總堆內存占用量計數器減去移出的bitmap大小
evictionCount++;//回收計數器加一
}
}
}
這個LRU緩存的最核心方法就這樣分析完了,其實原理很簡單,就是每放一個bitmap進LRU緩存都會記一下這個bitmap的大小,並計算當前LRU的總大小,如果發現總大小太大,就從棧底一個一個的把長時間沒用的bitmap給回收掉
那麼Picasso如何規定最大內存占用量的呢,讓我們來看代碼
/** Create a cache using an appropriate portion of the available RAM as the maximum size. */
public LruCache(Context context) {
this(Utils.calculateMemoryCacheSize(context));
}這個LRU緩存類在構造的時候就規定了最大內存占用指標,關鍵就是這個Utils.calculateMemoryCacheSize()方法,我們來看看它是怎麼規定的
static int calculateMemoryCacheSize(Context context) {
ActivityManager am = getService(context, ACTIVITY_SERVICE);
boolean largeHeap = (context.getApplicationInfo().flags & FLAG_LARGE_HEAP) != 0;
int memoryClass = am.getMemoryClass();
if (largeHeap && SDK_INT >= HONEYCOMB) {
memoryClass = ActivityManagerHoneycomb.getLargeMemoryClass(am);
}
// Target ~15% of the available heap.
return 1024 * 1024 * memoryClass / 7;
} 這裡是使用Context獲得一個ActitityManager,然後用其獲得獲得一個以MB為單位的APP可占最大堆內存占用大小,然後使用這個最大APP占用的七分之一來做當前圖片LRU緩存的最大可用大小,這個最大可用大小當然會隨著手機配置的提高而變大,目前我這邊測得的數據是:
紅米note3: 19MB(3G內存)
Sony L50:22MB(3G內存)
紅米2A:17MB(2G內存)
以經驗來看,這樣的內存分配並不大,經常出現在一個listView或者RecyclerView中,滑到底部後再滑回來,上面的元素的bitmap已經沒有的情況,以RGB_8888為例,一個像素占用的大小為32字節,那麼一個1920*1080的桌面背景圖片所占得堆內存大小是1920*1080*32 = 63MB,對於這樣圖,LRU幾乎是不會緩存的。
關於銷毀指定LRU緩存:
手動銷毀Picasso提供的默認LRU實現只能做到根據圖片url進行銷毀,而不能根據某個Activity或者Fragment進行銷毀,如果想實現按照頁面銷毀的話,需要自己重寫這個LruCache的實現。下面來看一下根據url進行銷毀的源碼:
@Override public final synchronized void clearKeyUri(String uri) {
boolean sizeChanged = false;
int uriLength = uri.length();
for (Iterator<Map.Entry<String, Bitmap>> i = map.entrySet().iterator(); i.hasNext();) {
Map.Entry<String, Bitmap> entry = i.next();
String key = entry.getKey();
Bitmap value = entry.getValue();
int newlineIndex = key.indexOf(KEY_SEPARATOR);
if (newlineIndex == uriLength && key.substring(0, newlineIndex).equals(uri)) {//加快尋找速度
i.remove();//將相應的url所對bitmap移出LRU緩存
size -= Utils.getBitmapBytes(value);//將當前總堆內存占用計數器減小被移出的bitmap大小
sizeChanged = true;
}
}
if (sizeChanged) {
trimToSize(maxSize);//移出後執行以下內存檢查,如果還是過大就繼續銷毀棧底的bitmap
}
}
我們在這裡發現,Picasso默認的LRU緩存方案並不是我們需要的或者適合自己項目的方案,最好的方法是根據自己APP特點和業務需要重寫LruCache,然後換掉Picasso默認的實現方案,方法如下:
Picasso.Builder builder = new Picasso.Builder(getContext());
builder.memoryCache(new CustomLruCache());//設置自定義的緩存方案
Picasso mPicasso = builder.build();//注意自定義Picasso實例要做成全局單例靜態,否則緩存會失效同樣方法可以自定義下載器,攔截器,線程池等等功能。
分析完這些實現,我們發現Picasso的強大之處並不在於針對某些應用場景提供完美的解決方案,而是它提供了一套完善的接口,讓我們自由的根據自己APP的實際情況去自定義我們自己的策略,要想用好Picasso,光用的默認的方法是不行的,更重要的是了解圖片下載、緩存、呈現的一系列需求並自定義自己的方案,然後借助Picasso來加載咱們自己的設定。
更多Android相關信息見Android 專題頁面 http://www.linuxidc.com/topicnews.aspx?tid=11