某年某月某日,糖葫蘆同學在掘金app上看了幾篇文章,偶然看到了一篇熟悉的詞LRU演算法,腦海裡就想這不是經常說的嘛,就那麼回事,當天晚上睡覺,LRU演算法是啥來著,好像是什麼最近最少使用的,白天在地鐵上看的文章也不少,但是到晚上想想好像啥也沒記住,就記得LRU演算法,我發現人大多數是這樣的啊,對於自己熟悉的部分呢還能記著點,不熟悉或者不會的可能真的是看過就忘啊~既然這樣還不如先把熟悉的弄明白。
第二天來到公司,我覺著還是有必要看一下這個LRU的原始碼,到底是怎麼回事,嗯,糖葫蘆同學刷刷得看,下面我們將進入正題,請戴眼鏡的同學把眼鏡擦一擦,哈哈哈
First
先看原始碼,再用具體的demo加以驗證,我們先看一下這個LruCache這個類的大致結構和方法,如下圖所示:
這又是 get(K),put(K,V), remove(K) 的方法的 給人的感覺就像是一個Map的集合嘛,又有Key ,又有value 的,再看下具體的程式碼:
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
/** Size of this cache in units. Not necessarily the number of elements. */
private int size;
private int maxSize;
private int putCount;
private int createCount;
private int evictionCount;
private int hitCount;
private int missCount;
/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is
* the maximum number of entries in the cache. For all other caches,
* this is the maximum sum of the sizes of the entries in this cache.
*/
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類中維護一個LinkedHashMap的一個集合,快取我們這個物件,而且構造方法裡需要我們傳入一個maxSize
的一個值,根據上面的註釋我們就明白了這個就是我們LruCache快取物件的最大數目。
有什麼用呢?
根據慣性思維,我們可以認為,在put
新的快取物件的時候,根據我們設定的最大值remove
集合裡的某些快取物件,進而新增新的快取物件。
Second
根據我們的分析,我們有必要去看一下這個put
方法的原始碼:
/**
* Caches {@code value} for {@code key}. The value is moved to the head of
* the queue.
*
* @return the previous value mapped by {@code key}.
*/
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;
}
複製程式碼
程式碼量也不是特別多,我們看下這個,在這個synchronized
同步程式碼塊裡,我們看到這個 size
,是對put進來快取物件個數的累加,然後呼叫集合的map.put
方法,返回一個物件 previous
,就是判斷這個集合中是否新增了這個快取物件,如果不為null,就對size
減回去。
最後又呼叫一個 trimToSize(maxSize)
方法,上面都是對新增一些邏輯的處理,那麼不可能無限制新增啊,肯定有移除操作,那麼我們推測這個邏輯可能在這個trimToSize(maxSize)
裡處理。
原始碼如下:
/**
* Remove the eldest entries until the total of remaining entries is at or
* below the requested size.
*
* @param maxSize the maximum size of the cache before returning. May be -1
* to evict even 0-sized elements.
*/
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!");
}
//只要當前size<= maxSize 就結束迴圈
if (size <= maxSize || map.isEmpty()) {
break;
}
// 獲取這個物件,然後從map中移除掉,保證size<=maxSize
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
複製程式碼
註釋:Remove the eldest entries until the total of remaining entries is at or below the requested size
大概意思是說:清除時間最久的物件直到剩餘快取物件的大小小於設定的大小。沒錯是我們想找的。
這裡說明一下:maxSize就是我們在構造方法裡傳入的,自己設定的
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的核心方法 trimToSize
方法我們就說完了,接下來我將通過例項再次驗證下:
設定場景
假設我們設定maxSize 為2,佈局裡顯示3個imageView,分別代表3張我們要顯示的圖片,我們新增3張圖片,看看會不會顯示3張?
xml佈局顯示如下(程式碼就不貼了,很簡單):
activity程式碼如下:
public final int MAX_SIZE = 2;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_lru);
ImageView iv1 = (ImageView) findViewById(R.id.iv1);
ImageView iv2 = (ImageView) findViewById(R.id.iv2);
ImageView iv3 = (ImageView) findViewById(R.id.iv3);
Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(),R.drawable.bg);
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(),R.drawable.header_img);
Bitmap bitmap3 = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher);
LruCache<String,Bitmap> lruCache = new LruCache<>(MAX_SIZE);
lruCache.put("1",bitmap1);
lruCache.put("2",bitmap2);
lruCache.put("3",bitmap3);
Bitmap bitmap = lruCache.get("1");
iv1.setImageBitmap(bitmap);
Bitmap b2 = lruCache.get("2");
iv2.setImageBitmap(b2);
Bitmap b3 = lruCache.get("3");
iv3.setImageBitmap(b3);
}
複製程式碼
圖:
我們可以先嚐試分析一下:因為我們設定的MaxSize 是2 ,那麼在put第三個Bitmap的時候,在trimToSize
方法中,發現這個size是3 ,maxSize 是2,會繼續向下執行,不會break,結合下面程式碼看下
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!");
}
//第一次迴圈:此時 size 是3,maxSize 是 2
//第二次迴圈,此時 size 是 2 ,maxSize 是 2 ,滿足條件,break,結束迴圈
if (size <= maxSize || map.isEmpty()) {
break;
}
//獲取最先新增的第一個元素
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
//移除掉第一個快取物件
map.remove(key);
// size = 2,減去移除的元素
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
複製程式碼
這個 safeSizeOf
是呼叫sizeOf
方法。
那麼也就是說,我們在put
第三個bitmap
的時候,LruCache
會自動幫我們移除掉第一個快取物件,因為第一個最先新增進去,時間也最長,當然後新增的bitmap
就是新的,最近的,那麼我們推斷這個iv1
是顯示不出圖片的,因為被移除掉了,其它剩餘兩個可以顯示,分析就到這裡,看下執行結果是不是跟我們分析的一樣:
哇!真的跟我們想的一樣耶,證明我們想的是對的。這裡我們思考一下就是為什麼LruCache
使用了這個LinkedHashMap
,為什麼LinkedHashMap
的創造方法跟我們平時建立的不太一樣,原始碼是這樣的:
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);
}
複製程式碼
這裡說一下評論裡
藏地情人
評論是:new LinkedHashMap<K, V>(0, 0.75f, true)
這句程式碼表示,初始容量為零,0.75
是載入因子,表示容量達到最大容量的75%
的時候會把記憶體增加一半。最後這個引數至關重要。表示訪問元素的排序方式,true
表示按照訪問順序排序,false
表示按照插入的順序排序。這個設定為true
的時候,如果對一個元素進行了操作(put、get)
,就會把那個元素放到集合的最後。
確實也是這樣的,我們看下LinkedHashMap
的原始碼:
/**
* Constructs an empty <tt>LinkedHashMap</tt> instance with the
* specified initial capacity, load factor and ordering mode.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @param accessOrder the ordering mode - <tt>true</tt> for
* access-order, <tt>false</tt> for insertion-order
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
複製程式碼
裡面這個assessOrder
註釋裡也說的很明白:the ordering mode - <tt>true</tt> for * access-order, <tt>false</tt> for insertion-order
-> true
呢就表示會排序,false
就代表按照插入的順序。預設不傳就是 false
,而且我們每次 get(K) put(K,V)
的時候 會根據這個變數調整元素在集合裡的位置。而這麼做的目的也只有一個:保留最近使用的快取物件,舉個例子說明一下:
我們向這個集合裡新增了三種元素
LruCache<String, Bitmap> lruCache = new LruCache<>(MAX_SIZE);(MAX_SIZE=2)
lruCache.put("1", bitmap1);
lruCache.put("2", bitmap2);
lruCache.put("3", bitmap3);
複製程式碼
此時它們在集合裡的順序是這樣的:
那比如說我們在put
3 元素之前,使用了1元素,就是呼叫了get("1")
方法,我們知道LinkedHashMap就會改變連結串列裡元素的儲存順序,程式碼是這樣的:
lruCache.put("1", bitmap1);
lruCache.put("2", bitmap2);
lruCache.get("1");
lruCache.put("3", bitmap3);
複製程式碼
那麼此時對應連結串列裡的順序就是:
複製程式碼
當我們再呼叫顯示的時候,迴圈遍歷就會優先把第一個位置的key = "2"
的快取物件移除掉,保證了最近使用的原則,當然了因為把這個max_size = 2
所以在我們執行lruCache.put("3", bitmap3);
時,集合最終會變成這樣:
集合裡只剩下 1 ,3
對應的快取物件。
至此,LruCache就說完了,如果看完的你有不明白的地方可以留言,一起討論下~