LruCache 是 Android 提供的一種基於內存的緩存框架。LRU 是 Least Recently Used 的縮寫,即最近最少使用。當一塊內存最近不多使用的時候就會被從緩存中移除。在這篇文章中,咱們會先簡單介紹 LruCache 的使用,而後咱們會對它的源碼進行分析。java
首先,讓咱們來簡單介紹一下如何使用 LruCache 實現內存緩存。下面是 LruCache 的一個使用示例。node
這裏咱們實現的是對 RecyclerView 的列表的截圖的功能。由於咱們須要將列表的每一個項的 Bitmap 存儲下來,而後當全部的列表項的 Bitmap 都拿到的時候,將其按照順序和位置繪製到一個完整的 Bitmap 上面。若是咱們不使用 LruCache 的話,固然也可以是實現這個功能——將全部的列表項的 Bitmap 放置到一個 List 中便可。可是那種方式存在缺點:由於是強引用類型,因此當內存不足的時候會致使 OOM。git
在下面的方法中,咱們先獲取了內存的大小的 8 分之一做爲緩存空間的大小,用來初始化 LruCache 對象,而後從 RecyclerView 的適配器中取出全部的 ViewHolder 並獲取其對應的 Bitmap,而後按照鍵值對的方式將其放置到 LruCache 中。當全部的列表項的 Bitmap 都拿到以後,咱們再建立最終的 Bitmap 並將以前的 Bitmap 依次繪製到最終的 Bitmap 上面:github
public static Bitmap shotRecyclerView(RecyclerView view) {
RecyclerView.Adapter adapter = view.getAdapter();
Bitmap bigBitmap = null;
if (adapter != null) {
int size = adapter.getItemCount();
int height = 0;
Paint paint = new Paint();
int iHeight = 0;
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 使用內存的 8 分之一做爲該緩存框架的緩存空間
final int cacheSize = maxMemory / 8;
LruCache<String, Bitmap> bitmaCache = new LruCache<>(cacheSize);
for (int i = 0; i < size; i++) {
RecyclerView.ViewHolder holder = adapter.createViewHolder(view, adapter.getItemViewType(i));
adapter.onBindViewHolder(holder, i);
holder.itemView.measure(
View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
holder.itemView.layout(0, 0, holder.itemView.getMeasuredWidth(),
holder.itemView.getMeasuredHeight());
holder.itemView.setDrawingCacheEnabled(true);
holder.itemView.buildDrawingCache();
Bitmap drawingCache = holder.itemView.getDrawingCache();
if (drawingCache != null) {
bitmaCache.put(String.valueOf(i), drawingCache);
}
height += holder.itemView.getMeasuredHeight();
}
bigBitmap = Bitmap.createBitmap(view.getMeasuredWidth(), height, Bitmap.Config.ARGB_8888);
Canvas bigCanvas = new Canvas(bigBitmap);
Drawable lBackground = view.getBackground();
if (lBackground instanceof ColorDrawable) {
ColorDrawable lColorDrawable = (ColorDrawable) lBackground;
int lColor = lColorDrawable.getColor();
bigCanvas.drawColor(lColor);
}
for (int i = 0; i < size; i++) {
Bitmap bitmap = bitmaCache.get(String.valueOf(i));
bigCanvas.drawBitmap(bitmap, 0f, iHeight, paint);
iHeight += bitmap.getHeight();
bitmap.recycle();
}
}
return bigBitmap;
}
複製代碼
所以,咱們能夠總結出 LruCahce 的基本用法以下:數組
首先,你要聲明一個緩存空間的大小,在這裏咱們用了運行時內存的 8 分之 1 做爲緩存空間的大小緩存
LruCache<String, Bitmap> bitmaCache = new LruCache<>(cacheSize);
複製代碼
可是應該注意的一個問題是緩存空間的單位的問題。由於 LruCache 的鍵值對的值多是任何類型的,因此你傳入的類型的大小如何統計須要本身去指定。後面咱們在分析它的源碼的時候會指出它的單位的問題。LruCahce 的 API 中也已經提供了計算傳入的值的大小的方法。咱們只須要在實例化一個 LruCache 的時候覆寫該方法便可。而這裏咱們認爲一個 Bitmap 對象所佔用的內存的大小不超過 1KB.安全
而後,咱們能夠像普通的 Map 同樣調用它的 put()
和 get()
方法向緩存中插入和從緩存中取出數據:數據結構
bitmaCache.put(String.valueOf(i), drawingCache);
Bitmap bitmap = bitmaCache.get(String.valueOf(i));
複製代碼
在咱們對 LruCache 的源碼進行分析以前,咱們現來考慮一下當咱們本身去實現一個 LruCache 的時候須要考慮哪些東西,以此來帶着問題閱讀源碼。框架
由於咱們須要對數據進行存儲,而且又可以根據指定的 id 將數據從緩存中取出,因此咱們須要使用哈希表表結構。或者使用兩個數組,一個做爲鍵一個做爲值,而後使用它們的索引來實現映射也行。可是,後者的效率不如前者高。源碼分析
此外,咱們還要對插入的元素進行排序,由於咱們須要移除那些使用頻率最小的元素。咱們可使用鏈表來達到這個目的,每當一個數據被用到的時候,咱們能夠將其移向鏈表的頭節點。這樣當要插入的元素大於緩存的最大空間的時候,咱們就將鏈表末位的元素移除,以在緩存中騰出空間。
綜合這兩點,咱們須要一個既有哈希表功能,又有隊列功能的數據結構。在 Java 的集合中,已經爲咱們提供了 LinkedHashMap 用來實現這個功能。
實際上在 Android 中的 LruCache 也正是使用 LinkedHashMap 來實現的。LinkedHashMap 拓展自 HashMap。若是理解 HashMap 的話,它的源碼就不難閱讀。LinkedHashMap 僅在 HashMap 的基礎之上,又將各個節點放進了一個雙向鏈表中。每次增長和刪除一個元素的時候,被操做的元素會被移到到鏈表的末尾。Android 中的 LruCahce 就是在 LinkedHashMap 基礎之上進行了一層拓展,不過 Android 中的 LruCache 的實現具備一些很巧妙的地方值得咱們學習。
從上面的分析中咱們知道了選擇 LinkedHashMap 做爲底層數據結構的緣由。下面咱們分析其中的一些方法。這個類的實現還有許多的細節考慮得很是周到,很是值得咱們借鑑和學習。
在 LruCache 中有兩個字段 size 和 maxSize. maxSize 會在 LruCache 的構造方法中被賦值,用來表示該緩存的最大可用的空間:
int cacheSize = 4 * 1024 * 1024; // 4MiB,cacheSize 的單位是 KB
LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
}};
複製代碼
這裏咱們使用 4MB 來設置緩存空間的大小。咱們知道 LruCache 的原理是指定了空間的大小以後,若是繼續插入元素時,空間超出了指定的大小就會將那些「能夠被移除」的元素移除掉,以此來爲新的元素騰出空間。那麼,由於插入的類型時不肯定的,因此具體被插入的對象如何計算大小就應該交給用戶來實現。
在上面的代碼中,咱們直接使用了 Bitmap 的 getByteCount()
方法來獲取 Bitmap 的大小。同時,咱們也注意到在最初的例子中,咱們並無這樣去操做。那樣的話一個 Bitmap 將會被看成 1KB 來計算。
這裏的 sizeOf() 是一個受保護的方法,顯然是但願用戶本身去實現計算的邏輯。它的默認值是 1,單位和設置緩存大小指定的 maxSize 的單位相同:
protected int sizeOf(K key, V value) {
return 1;
}
複製代碼
這裏咱們還須要說起一下:雖然這個方法交給用戶來實現,可是在 LruCache 的源碼中,不會直接調用這個方法,而是
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
複製代碼
因此,這裏又增長了一個檢查,防止參數錯誤。其實,這個考慮是很是周到的,試想若是傳入了一個非法的參數,致使了意外的錯誤,那麼錯誤的地方就很難跟蹤了。若是咱們本身想設計 API 給別人用而且提供給他們本身能夠覆寫的方法的時候,不妨借鑑一下這個設計。
下面咱們分析它的 get() 方法。它用來從 LruCahce 中根據指定的鍵來獲取對應的值:
/** * 1). 獲取指定 key 對應的元素,若是不存在的話就用 craete() 方法建立一個。 * 2). 當返回一個元素的時候,該元素將被移動到隊列的首位; * 3). 若是在緩存中不存在又不能建立,就返回n ull */
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
// 在這裏若是返回不爲空的話就會將返回的元素移動到隊列頭部,這是在 LinkedHashMap 中實現的
mapValue = map.get(key);
if (mapValue != null) {
// 緩存命中
hitCount++;
return mapValue;
}
// 緩存沒有命中,多是由於這個鍵值對被移除了
missCount++;
}
// 這裏的建立是單線程的,在建立的時候指定的 key 可能已經被其餘的鍵值對佔用
V createdValue = create(key);
if (createdValue == null) {
return null;
}
// 這裏設計的目的是防止建立的時候,指定的 key 已經被其餘的 value 佔用,若是衝突就撤銷插入
synchronized (this) {
createCount++;
// 向表中插入一個新的數據的時候會返回該 key 以前對應的值,若是沒有的話就返回 null
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// 衝突了,還要撤銷以前的插入操做
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
複製代碼
這裏獲取值的時候對當前的實例進行了加鎖以保證線程安全。當用 map 的 get() 方法獲取不到數據的時候用了 create()
方法。由於當指定的鍵值對找不到的時候,可能它原本就不存在,多是由於緩存不足被移除了,因此,咱們須要提供這個方法讓用戶來處理這種狀況,該方法默認返回 null. 若是用戶覆寫了 create()
方法,而且返回的值不爲 null,那麼咱們須要將該值插入到哈希表中。
插入的邏輯也在同步代碼塊中進行。這是由於,建立的操做可能過長並且是非同步的。當咱們再次向指定的 key 插入值的時候,它可能已經存在值了。因此當調用 map 的 put() 的時候若是返回不爲 null,就代表對應的 key 已經有對應的值了,就須要撤銷插入操做。最後,當 mapValue 非 null,還要調用 entryRemoved()
方法。每當一個鍵值對從哈希表中被移除的時候,這個方法將會被回調一次。
最後調用了 trimToSize()
方法,用來保證新的值被插入以後緩存的空間大小不會超過咱們指定的值。當發現已經使用的緩存超出最大的緩存大小的時候,「最近最少使用」 的項目將會被從哈希表中移除。
那麼如何來判斷哪一個是 「最近最少使用」 的項目呢?咱們先來看下 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 Map.Entry<K, V> eldest() {
return head;
}
複製代碼
也就是 LinkedHashMap 的頭結點。那麼爲何要移除頭結點呢?這不符合 LRU 的原則啊,這裏分明是直接移除了頭結點。實際上不是這樣,魔力發生在 get()
方法中。在 LruCache 的 get() 方法中,咱們調用了 LinkedHashMap 的 get()
方法,這個方法中又會在拿到值的時候調用下面的方法:
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMapEntry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
複製代碼
這裏的邏輯是把 get()
方法中返回的結點移動到雙向鏈表的末尾。因此,最近最少使用的結點必然就是頭結點了。
以上是咱們對 LruCache 的是使用和源碼的總結,這裏咱們實際上只分析了 get()
的過程。由於這個方法纔是 LruCache 的核心,它包含了插入值和移動最近使用的項目的過程。至於 put()
和 remove()
兩種方法,它們內部實際上直接調用了 LinkedHashMap 的方法。這裏咱們再也不對它們進行分析。
若是您喜歡個人文章,能夠在如下平臺關注我: