您好,登錄后才能下訂單哦!
本篇文章為大家展示了Android中如何使用LruCache內存緩存框架,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
1、基本的使用示例
首先,讓我們來簡單介紹一下如何使用 LruCache 實現內存緩存。下面是 LruCache 的一個使用示例。
這里我們實現的是對 RecyclerView 的列表的截圖的功能。因為我們需要將列表的每個項的 Bitmap 存儲下來,然后當所有的列表項的 Bitmap 都拿到的時候,將其按照順序和位置繪制到一個完整的 Bitmap 上面。如果我們不使用 LruCache 的話,當然也能夠是實現這個功能——將所有的列表項的 Bitmap 放置到一個 List 中即可。但是那種方式存在缺點:因為是強引用類型,所以當內存不足的時候會導致 OOM。
在下面的方法中,我們先獲取了內存的大小的 8 分之一作為緩存空間的大小,用來初始化 LruCache 對象,然后從 RecyclerView 的適配器中取出所有的 ViewHolder 并獲取其對應的 Bitmap,然后按照鍵值對的方式將其放置到 LruCache 中。當所有的列表項的 Bitmap 都拿到之后,我們再創建最終的 Bitmap 并將之前的 Bitmap 依次繪制到最終的 Bitmap 上面:
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));
2、LruCahce 源碼分析
2.1 分析之前:當我們自己實現一個 LruCache 的時候,我們需要考慮什么
在我們對 LruCache 的源碼進行分析之前,我們現來考慮一下當我們自己去實現一個 LruCache 的時候需要考慮哪些東西,以此來帶著問題閱讀源碼。
因為我們需要對數據進行存儲,并且又能夠根據指定的 id 將數據從緩存中取出,所以我們需要使用哈希表表結構。或者使用兩個數組,一個作為鍵一個作為值,然后使用它們的索引來實現映射也行。但是,后者的效率不如前者高。
此外,我們還要對插入的元素進行排序,因為我們需要移除那些使用頻率最小的元素。我們可以使用鏈表來達到這個目的,每當一個數據被用到的時候,我們可以將其移向鏈表的頭節點。這樣當要插入的元素大于緩存的最大空間的時候,我們就將鏈表末位的元素移除,以在緩存中騰出空間。
綜合這兩點,我們需要一個既有哈希表功能,又有隊列功能的數據結構。在 Java 的集合中,已經為我們提供了 LinkedHashMap 用來實現這個功能。
實際上在 Android 中的 LruCache 也正是使用 LinkedHashMap 來實現的。LinkedHashMap 拓展自HashMap。如果理解 HashMap 的話,它的源碼就不難閱讀。LinkedHashMap 僅在 HashMap 的基礎之上,又將各個節點放進了一個雙向鏈表中。每次增加和刪除一個元素的時候,被操作的元素會被移到到鏈表的末尾。Android 中的 LruCahce 就是在 LinkedHashMap 基礎之上進行了一層拓展,不過 Android 中的 LruCache 的實現具有一些很巧妙的地方值得我們學習。
2.2 LruCache 源代碼分析
從上面的分析中我們知道了選擇 LinkedHashMap 作為底層數據結構的原因。下面我們分析其中的一些方法。這個類的實現還有許多的細節考慮得非常周到,非常值得我們借鑒和學習。
2.2.1 緩存的最大可用空間
在 LruCache 中有兩個字段 size 和 maxSize. maxSize 會在 LruCache 的構造方法中被賦值,用來表示該緩存的最大可用的空間:
int cacheSize = 4 * 1024 * 1024; // 4MiB,cacheSize 的單位是 KBLruCache<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 給別人用并且提供給他們自己可以覆寫的方法的時候,不妨借鑒一下這個設計。
2.2.2 LruCache 的 get() 方法
下面我們分析它的 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; } }
上述內容就是Android中如何使用LruCache內存緩存框架,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。