緩存策略在移動端設備上是非常重要的,尤其是在圖片加載這個場景下,因爲圖片相對而言比較大會花費用戶較多的流量,因此可用緩存方式來解決,即當程序第一次從網絡上獲取圖片的時候,就將其緩存到存儲設備上,這樣在用戶下次使用這張圖片時就不用從網絡上再次獲取,這樣就能爲用戶節省一定的流量。這個功能目前絕大部分主流APP都會使用,如騰訊QQ,微信。但很多時候爲了提高APP的用戶體驗,我們還需要把圖片在內存中緩存一份,比如ListView,我們知道LIstView會在用戶將某些圖片移出屏幕後將其進行回收,此時垃圾回收器會認爲你不再持有這些圖片的引用,從而對這些圖片進行GC操作。可是爲了能讓程序快速運行,在界面上迅速地加載圖片,必須要考慮到某些圖片被回收之後,用戶又將它重新滑入屏幕這種情況,而這種情況在ListView,GridView這種控件中出現是非常頻繁的。採用內存緩存技術可以很好的解決上述問題,內存緩存技術允許控件可以快速地重新加載那些處理過的圖片。內存緩存技術主要是通過LruCache這個類來完成的,下面從源碼的角度詳細講解LruCache這個類,然後在此基礎上講解如何使用LruCache,讓讀者知其然更知其所以然。
一LruCache類:
首先我們來看一下類的定義及其構造函數:
public class LruCache<K, V>
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
可以看到LruCache類使用泛型參數,其構造器參數爲int型,該參數用來指定LruCache的大小,另外從其構造函數的實現過程來看,可以知道LruCache的底層是使用LinkedHashMap<K, V>來實現的,即LruCache使用一個強引用(strong referenced)的LinkedHashMap保存最近引用的對象。(A cache that holds strong references to a limited number of values.)
然後我麼來看一下其重要的方法:
public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
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;
}
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
/*
* Attempt to create a value. This may take a long time, and the map
* may be different when create() returns. If a conflicting value was
* added to the map while create() was working, we leave that value in
* the map and release the created value.
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
protected int sizeOf(K key, V value) {
return 1;
}
之所以把這三個方法拿出來講解,是因爲這三個方法它對應着我們實現的LruCache內存緩存策略的創建緩存,添加key到緩存,從緩存中獲取V這三個最重要的功能,通常我們在創建緩存時會重寫上述的sizeOf(K key, V value),在該方法中返回我們要創建的內存緩存的大小。而put與get則相對複雜些,我們首先來分析一下put方法。
可以看到put方法作用是緩存key,同時會將key移動到隊列頭部Caches {@code value} for {@code key}. The value is moved to the head of the queue.,另外它會返回與的key對應的先前的V,(return the previous value mapped by {@code key})。採用的是同步方式來實現的。
接下來我們看一下get方法。
從get方法中可以看到get包括兩種情況:
1取的時候命中:直接返回與key對應的V。
2取的時候未命中:根據key產生一個V,然後調用put方法將其放入。
當get方法會將返回的值移動到隊列頭部。If a value was returned, it is moved to the head of the queue。
從put與get方法可以看到,put與get時會將該元素放到隊列的頭部,因爲無論是put還是get都表示該元素目前被使用過,所以會將其放到隊列的頭部,當緩存中的元素超過maxSize時,會通過trimToSize函數來去除緩存中最久的元素( Map.Entry<K, V> toEvict = map.eldest();),這就是所謂的LRU算法,即最近最少使用算法,即噹噹緩存中的元素超過maxSize時會淘汰最近最少使用的元素。下面是trimToSize的源碼:
public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}
if (size <= maxSize) {
break;
}
Map.Entry<K, V> toEvict = map.eldest();//首先獲取最久的元素
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);//去除最久的元素(也是未被使用的最久的元素)
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
下面是LinkedHashMap中eldest()函數的代碼:
public Entry<K, V> eldest() {
LinkedEntry<K, V> eldest = header.nxt;
return eldest != header ? eldest : null;
}
通過上述代碼的分析相信看官對LruCache的思想已經非常熟悉了,LruCache的底層是通過LinkedHashMap來實現的,當創建一個LruCache的對象時會讓我們傳入一個int型的maxSize,當我們向LruCache中put與get元素時會將該元素放到緩存隊列的對頭,當put元素超過maxSize時(這也是爲何要傳入maxSize參數的原因),會通過trimToSize函數來去除緩存中最久的元素,具體是通過Map.Entry<K, V> toEvict = map.eldest();來獲取最久的元素,然後通過remove(key)的方式將其移除。
二LruCache的使用:
這個類是3.1版本中提供的,如果要在更早的Android版本中使用,則需要導入android-support-v4的jar包。
我們以BitMap對象來創建LruCache緩存爲例,首先正如我在前面講述的,一個LruCache緩存策略至少包含三個功能,即創建緩存,向緩存中添加元素,從緩存中獲取元素。
代碼如下:
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.getByteCount() / 1024;
}
};
...
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
我們說過緩存的目的就是爲了當加載某個圖片時首先從LruCache 中檢查是否存在這個Bitmap。如果確實存在,它會立即被用來顯示到ImageView上,如果不存在,則會開啓一個後臺線程去處理顯示該Bitmap任務。所以我們還需要爲其添加一個loadBitmap的功能,代碼如下:
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
mImageView.setImageBitmap(bitmap);
} else {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}
其中的BitmapWorkerTask 需要把解析好的Bitmap添加到內存緩存中:
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
...
}