本篇文章為大家展示了Android中如何使用LruCache內(nèi)存緩存框架,內(nèi)容簡(jiǎn)明扼要并且容易理解,絕對(duì)能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。
在永安等地區(qū),都構(gòu)建了全面的區(qū)域性戰(zhàn)略布局,加強(qiáng)發(fā)展的系統(tǒng)性、市場(chǎng)前瞻性、產(chǎn)品創(chuàng)新能力,以專注、極致的服務(wù)理念,為客戶提供成都做網(wǎng)站、成都網(wǎng)站建設(shè) 網(wǎng)站設(shè)計(jì)制作按需制作,公司網(wǎng)站建設(shè),企業(yè)網(wǎng)站建設(shè),成都品牌網(wǎng)站建設(shè),網(wǎng)絡(luò)營(yíng)銷推廣,外貿(mào)網(wǎng)站制作,永安網(wǎng)站建設(shè)費(fèi)用合理。
1、基本的使用示例
首先,讓我們來簡(jiǎn)單介紹一下如何使用 LruCache 實(shí)現(xiàn)內(nèi)存緩存。下面是 LruCache 的一個(gè)使用示例。
這里我們實(shí)現(xiàn)的是對(duì) RecyclerView 的列表的截圖的功能。因?yàn)槲覀冃枰獙⒘斜淼拿總€(gè)項(xiàng)的 Bitmap 存儲(chǔ)下來,然后當(dāng)所有的列表項(xiàng)的 Bitmap 都拿到的時(shí)候,將其按照順序和位置繪制到一個(gè)完整的 Bitmap 上面。如果我們不使用 LruCache 的話,當(dāng)然也能夠是實(shí)現(xiàn)這個(gè)功能——將所有的列表項(xiàng)的 Bitmap 放置到一個(gè) List 中即可。但是那種方式存在缺點(diǎn):因?yàn)槭菑?qiáng)引用類型,所以當(dāng)內(nèi)存不足的時(shí)候會(huì)導(dǎo)致 OOM。
在下面的方法中,我們先獲取了內(nèi)存的大小的 8 分之一作為緩存空間的大小,用來初始化 LruCache 對(duì)象,然后從 RecyclerView 的適配器中取出所有的 ViewHolder 并獲取其對(duì)應(yīng)的 Bitmap,然后按照鍵值對(duì)的方式將其放置到 LruCache 中。當(dāng)所有的列表項(xiàng)的 Bitmap 都拿到之后,我們?cè)賱?chuàng)建最終的 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); // 使用內(nèi)存的 8 分之一作為該緩存框架的緩存空間 final int cacheSize = maxMemory / 8; LruCachebitmaCache = 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; }
因此,我們可以總結(jié)出 LruCahce 的基本用法如下:
首先,你要聲明一個(gè)緩存空間的大小,在這里我們用了運(yùn)行時(shí)內(nèi)存的 8 分之 1 作為緩存空間的大小
LruCachebitmaCache = new LruCache<>(cacheSize);
但是應(yīng)該注意的一個(gè)問題是緩存空間的單位的問題。因?yàn)?LruCache 的鍵值對(duì)的值可能是任何類型的,所以你傳入的類型的大小如何統(tǒng)計(jì)需要自己去指定。后面我們?cè)诜治鏊脑创a的時(shí)候會(huì)指出它的單位的問題。LruCahce 的 API 中也已經(jīng)提供了計(jì)算傳入的值的大小的方法。我們只需要在實(shí)例化一個(gè) LruCache 的時(shí)候覆寫該方法即可。而這里我們認(rèn)為一個(gè) Bitmap 對(duì)象所占用的內(nèi)存的大小不超過 1KB.
然后,我們可以像普通的 Map 一樣調(diào)用它的 put() 和 get() 方法向緩存中插入和從緩存中取出數(shù)據(jù):
bitmaCache.put(String.valueOf(i), drawingCache); Bitmap bitmap = bitmaCache.get(String.valueOf(i));
2、LruCahce 源碼分析
2.1 分析之前:當(dāng)我們自己實(shí)現(xiàn)一個(gè) LruCache 的時(shí)候,我們需要考慮什么
在我們對(duì) LruCache 的源碼進(jìn)行分析之前,我們現(xiàn)來考慮一下當(dāng)我們自己去實(shí)現(xiàn)一個(gè) LruCache 的時(shí)候需要考慮哪些東西,以此來帶著問題閱讀源碼。
因?yàn)槲覀冃枰獙?duì)數(shù)據(jù)進(jìn)行存儲(chǔ),并且又能夠根據(jù)指定的 id 將數(shù)據(jù)從緩存中取出,所以我們需要使用哈希表表結(jié)構(gòu)?;蛘呤褂脙蓚€(gè)數(shù)組,一個(gè)作為鍵一個(gè)作為值,然后使用它們的索引來實(shí)現(xiàn)映射也行。但是,后者的效率不如前者高。
此外,我們還要對(duì)插入的元素進(jìn)行排序,因?yàn)槲覀冃枰瞥切┦褂妙l率最小的元素。我們可以使用鏈表來達(dá)到這個(gè)目的,每當(dāng)一個(gè)數(shù)據(jù)被用到的時(shí)候,我們可以將其移向鏈表的頭節(jié)點(diǎn)。這樣當(dāng)要插入的元素大于緩存的最大空間的時(shí)候,我們就將鏈表末位的元素移除,以在緩存中騰出空間。
綜合這兩點(diǎn),我們需要一個(gè)既有哈希表功能,又有隊(duì)列功能的數(shù)據(jù)結(jié)構(gòu)。在 Java 的集合中,已經(jīng)為我們提供了 LinkedHashMap 用來實(shí)現(xiàn)這個(gè)功能。
實(shí)際上在 Android 中的 LruCache 也正是使用 LinkedHashMap 來實(shí)現(xiàn)的。LinkedHashMap 拓展自HashMap。如果理解 HashMap 的話,它的源碼就不難閱讀。LinkedHashMap 僅在 HashMap 的基礎(chǔ)之上,又將各個(gè)節(jié)點(diǎn)放進(jìn)了一個(gè)雙向鏈表中。每次增加和刪除一個(gè)元素的時(shí)候,被操作的元素會(huì)被移到到鏈表的末尾。Android 中的 LruCahce 就是在 LinkedHashMap 基礎(chǔ)之上進(jìn)行了一層拓展,不過 Android 中的 LruCache 的實(shí)現(xiàn)具有一些很巧妙的地方值得我們學(xué)習(xí)。
2.2 LruCache 源代碼分析
從上面的分析中我們知道了選擇 LinkedHashMap 作為底層數(shù)據(jù)結(jié)構(gòu)的原因。下面我們分析其中的一些方法。這個(gè)類的實(shí)現(xiàn)還有許多的細(xì)節(jié)考慮得非常周到,非常值得我們借鑒和學(xué)習(xí)。
2.2.1 緩存的最大可用空間
在 LruCache 中有兩個(gè)字段 size 和 maxSize. maxSize 會(huì)在 LruCache 的構(gòu)造方法中被賦值,用來表示該緩存的最大可用的空間:
int cacheSize = 4 * 1024 * 1024; // 4MiB,cacheSize 的單位是 KBLruCachebitmapCache = new LruCache (cacheSize) { protected int sizeOf(String key, Bitmap value) { return value.getByteCount(); } }};
這里我們使用 4MB 來設(shè)置緩存空間的大小。我們知道 LruCache 的原理是指定了空間的大小之后,如果繼續(xù)插入元素時(shí),空間超出了指定的大小就會(huì)將那些“可以被移除”的元素移除掉,以此來為新的元素騰出空間。那么,因?yàn)椴迦氲念愋蜁r(shí)不確定的,所以具體被插入的對(duì)象如何計(jì)算大小就應(yīng)該交給用戶來實(shí)現(xiàn)。
在上面的代碼中,我們直接使用了 Bitmap 的 getByteCount() 方法來獲取 Bitmap 的大小。同時(shí),我們也注意到在最初的例子中,我們并沒有這樣去操作。那樣的話一個(gè) Bitmap 將會(huì)被當(dāng)作 1KB 來計(jì)算。
這里的 sizeOf() 是一個(gè)受保護(hù)的方法,顯然是希望用戶自己去實(shí)現(xiàn)計(jì)算的邏輯。它的默認(rèn)值是 1,單位和設(shè)置緩存大小指定的 maxSize 的單位相同:
protected int sizeOf(K key, V value) { return 1; }
這里我們還需要提及一下:雖然這個(gè)方法交給用戶來實(shí)現(xiàn),但是在 LruCache 的源碼中,不會(huì)直接調(diào)用這個(gè)方法,而是
private int safeSizeOf(K key, V value) { int result = sizeOf(key, value); if (result < 0) { throw new IllegalStateException("Negative size: " + key + "=" + value); } return result; }
所以,這里又增加了一個(gè)檢查,防止參數(shù)錯(cuò)誤。其實(shí),這個(gè)考慮是非常周到的,試想如果傳入了一個(gè)非法的參數(shù),導(dǎo)致了意外的錯(cuò)誤,那么錯(cuò)誤的地方就很難跟蹤了。如果我們自己想設(shè)計(jì) API 給別人用并且提供給他們自己可以覆寫的方法的時(shí)候,不妨借鑒一下這個(gè)設(shè)計(jì)。
2.2.2 LruCache 的 get() 方法
下面我們分析它的 get() 方法。它用來從 LruCahce 中根據(jù)指定的鍵來獲取對(duì)應(yīng)的值:
/** * 1). 獲取指定 key 對(duì)應(yīng)的元素,如果不存在的話就用 craete() 方法創(chuàng)建一個(gè)。 * 2). 當(dāng)返回一個(gè)元素的時(shí)候,該元素將被移動(dòng)到隊(duì)列的首位; * 3). 如果在緩存中不存在又不能創(chuàng)建,就返回n ull */public final V get(K key) { if (key == null) { throw new NullPointerException("key == null"); } V mapValue; synchronized (this) { // 在這里如果返回不為空的話就會(huì)將返回的元素移動(dòng)到隊(duì)列頭部,這是在 LinkedHashMap 中實(shí)現(xiàn)的 mapValue = map.get(key); if (mapValue != null) { // 緩存命中 hitCount++; return mapValue; } // 緩存沒有命中,可能是因?yàn)檫@個(gè)鍵值對(duì)被移除了 missCount++; } // 這里的創(chuàng)建是單線程的,在創(chuàng)建的時(shí)候指定的 key 可能已經(jīng)被其他的鍵值對(duì)占用 V createdValue = create(key); if (createdValue == null) { return null; } // 這里設(shè)計(jì)的目的是防止創(chuàng)建的時(shí)候,指定的 key 已經(jīng)被其他的 value 占用,如果沖突就撤銷插入 synchronized (this) { createCount++; // 向表中插入一個(gè)新的數(shù)據(jù)的時(shí)候會(huì)返回該 key 之前對(duì)應(yīng)的值,如果沒有的話就返回 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; } }
這里獲取值的時(shí)候?qū)Ξ?dāng)前的實(shí)例進(jìn)行了加鎖以保證線程安全。當(dāng)用 map 的 get() 方法獲取不到數(shù)據(jù)的時(shí)候用了 create() 方法。因?yàn)楫?dāng)指定的鍵值對(duì)找不到的時(shí)候,可能它本來就不存在,可能是因?yàn)榫彺娌蛔惚灰瞥?,所以,我們需要提供這個(gè)方法讓用戶來處理這種情況,該方法默認(rèn)返回 null. 如果用戶覆寫了 create() 方法,并且返回的值不為 null,那么我們需要將該值插入到哈希表中。
插入的邏輯也在同步代碼塊中進(jìn)行。這是因?yàn)?,?chuàng)建的操作可能過長(zhǎng)而且是非同步的。當(dāng)我們?cè)俅蜗蛑付ǖ?key 插入值的時(shí)候,它可能已經(jīng)存在值了。所以當(dāng)調(diào)用 map 的 put() 的時(shí)候如果返回不為 null,就表明對(duì)應(yīng)的 key 已經(jīng)有對(duì)應(yīng)的值了,就需要撤銷插入操作。最后,當(dāng) mapValue 非 null,還要調(diào)用 entryRemoved() 方法。每當(dāng)一個(gè)鍵值對(duì)從哈希表中被移除的時(shí)候,這個(gè)方法將會(huì)被回調(diào)一次。
最后調(diào)用了 trimToSize() 方法,用來保證新的值被插入之后緩存的空間大小不會(huì)超過我們指定的值。當(dāng)發(fā)現(xiàn)已經(jīng)使用的緩存超出最大的緩存大小的時(shí)候,“最近最少使用” 的項(xiàng)目將會(huì)被從哈希表中移除。
那么如何來判斷哪個(gè)是 “最近最少使用” 的項(xiàng)目呢?我們先來看下 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; } // 獲取用來移除的 “最近最少使用” 的項(xiàng)目 Map.EntrytoEvict = 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() 方法,這個(gè)方法的返回值是:
public Map.Entryeldest() { return head; }
也就是 LinkedHashMap 的頭結(jié)點(diǎn)。那么為什么要移除頭結(jié)點(diǎn)呢?這不符合 LRU 的原則啊,這里分明是直接移除了頭結(jié)點(diǎn)。實(shí)際上不是這樣,魔力發(fā)生在 get() 方法中。在 LruCache 的 get() 方法中,我們調(diào)用了 LinkedHashMap 的 get() 方法,這個(gè)方法中又會(huì)在拿到值的時(shí)候調(diào)用下面的方法:
void afterNodeAccess(Nodee) { // move node to last LinkedHashMapEntry last; if (accessOrder && (last = tail) != e) { LinkedHashMapEntry p = (LinkedHashMapEntry )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; } }
上述內(nèi)容就是Android中如何使用LruCache內(nèi)存緩存框架,你們學(xué)到知識(shí)或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識(shí)儲(chǔ)備,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。