說說Android LRU緩衝演算法實現學習筆記(一),androidlru
在我們的手機應用開發時候,我們經常會遇到大資料訪問的時候,我們通常會考慮以下幾個方面的情況。一、手機記憶體的限制還必須保證應用反應的流暢;二、盡量小的流量消耗,不然,你的應用流暢度再好體驗再好,使用者還是會毫不猶豫的卸載掉你的應用。大資料量訪問的情況下,資料緩衝是我們一定會考慮到的解決方案。而作為緩衝,我們很重要的會考慮以下幾點:1.訪問速度;2.逐出舊的緩衝策略;3.最好還能考慮到一定的並發度。這篇我們主要說說LRU策略的緩衝演算法實現,我們就用圖片緩衝為例來談談Android應用開發中的緩衝實現。
首先我們來看看Google官方的推薦的緩衝:在Android3.0加入的LruCache和 DiskLruCache(硬碟緩衝結構)類。我們從代碼的實現知道,LruCache和DiskLruCache緩衝的實現都是基於JDK的LinkedHashMap集合來實現。下面我們來從LinkedHashMap的源碼的分析來開始學習。
通過源碼我們知道,LinkedHashMap是繼承HashMap,底層結構不僅使用HashMap來儲存元素,同時通過繼承HashMapEntry 實現雙向鏈表的結構來關聯其他的元素。我們先來看LInkedHashMap的節點實現:
/** * LinkedHashMap entry. */ private static class Entry<K,V> extends HashMap.Entry<K,V> { // These fields comprise the doubly linked list used for iteration. Entry<K,V> before, after; LinkedHashMap的節點Entry繼承自HashMap.Entry來實現,增加兩個引用來分別指向前一個元素和後一個元素。LinkedHashMap的實現是在HashMap的基礎上增加雙鏈表的結構。
我們再來看看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; //accessOrder指定排序,預設為false,為fasle的時候,插入順序排序,為true時候,訪問順序排序 }我們看到重寫的基類的初始化方法init實習:
/** * Called by superclass constructors and pseudoconstructors (clone, * readObject) before any entries are inserted into the map. Initializes * the chain. */ void init() { header = new Entry<K,V>(-1, null, null, null); header.before = header.after = header; }此處初始化頭指標給迴圈指定。下面我們重點來看看集合最總要的兩個方法put和get的實現。我們先看put方法,LinkedHashMap的put方法並沒有重寫HashMap的put方法。只是重寫了put方法下的addEntry方法,addEntry方法執行時候是當LinkedHashMap插入新的結點的時候執行。
/** * This override alters behavior of superclass put method. It causes newly * allocated entry to get inserted at the end of the linked list and * removes the eldest entry if appropriate. */ void addEntry(int hash, K key, V value, int bucketIndex) { createEntry(hash, key, value, bucketIndex); // Remove eldest entry if instructed, else grow capacity if appropriate Entry<K,V> eldest = header.after; if (removeEldestEntry(eldest)) { removeEntryForKey(eldest.key); } else { if (size >= threshold) resize(2 * table.length); } } /** * This override differs from addEntry in that it doesn't resize the * table or remove the eldest entry. */ void createEntry(int hash, K key, V value, int bucketIndex) { HashMap.Entry<K,V> old = table[bucketIndex];Entry<K,V> e = new Entry<K,V>(hash, key, value, old); table[bucketIndex] = e; e.addBefore(header); size++; } 我們看我們產生新結點的方法creatEntry實現,我們先給找到雜湊表上對應的位置,然後給新的Entry指定前後的節點。執行方法e.addBefore(header)代碼如下:
/** * Inserts this entry before the specified existing entry in the list. */ private void addBefore(Entry<K,V> existingEntry) { after = existingEntry; before = existingEntry.before; before.after = this; after.before = this; } 我們看到代碼實現的功能就是把新結點添加到header結點前。我們再回到addEntry的代碼實現,我們看 Entry<K,V> eldest = header.after;if (removeEldestEntry(eldest))的執行,我們把header後結點理解成我們最舊的節點,removeEldestEntry(eldest)方法的實現:
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return false; }
removeEldestEntry方法執行預設返回false,意思就是我們LinkedHashMap預設不會逐去舊的節點。我們可以通過在子類重載來重寫removeEldestEntry的實現,來修改LRU策略。下面我們用圖示來表示put方法的執行一下代碼的執行過程:
map.put("22", "xxx"); 首先map執行前的結構是這樣:
為了不讓圖上密密麻麻的都是引用箭頭亂竄,我省略了HashMap的很多Entry結點的before和after的指向,我們只是重點表示header的引用指向。我們執行方法map.put("22","xxx")後的資料結構變成這樣:
我們把key為22的節點通過hash演算法,指定到eee的後面,同時我們把header的before指向修改為指向key為22的節點。分析完我們的put方法,我們接著來分析get方法的實現,代碼如下:
public V get(Object key) { Entry<K,V> e = (Entry<K,V>)getEntry(key); if (e == null) return null; e.recordAccess(this); return e.value; }我們看到get方法很簡單,首先我們去Map取key對應的value是否存在,如果不存在,我們返回null。如果存在,我們執行e.recoedAccess(this)方法。
/** * This method is invoked by the superclass whenever the value * of a pre-existing entry is read by Map.get or modified by Map.set. * If the enclosing Map is access-ordered, it moves the entry * to the end of the list; otherwise, it does nothing. */ void recordAccess(HashMap<K,V> m) { LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m; if (lm.accessOrder) { //此處的判斷LinkedHashMap的排序次序 lm.modCount++; remove(); addBefore(lm.header); } } recodeAccess方法中,當我們的排序次序為false,為按插入排序次序的時候,我們直接退出方法。當我們的排序次序為訪問排序次序的時候,我們執行remove方法,把結點從鏈表裡切除出來,然後執行addBefore方法時候,我們把結點插入到header結點的前面。這樣,我們就實習每次根據訪問位置來排序的操作。
下面我們通過圖的例子來說明,當LinkedHashMap的排序為訪問排序的時候,我們get操作的過程,LinkedHashMap的資料結構的變化。我們執行的代碼如下:
put.get("15");//我們假設key為15對應的value為"bbb"我們在執行完map.put("22","xxx")後,再執行put.get("15")的變化如下:
以上為執行get方法之後的代碼發生變化,"bbb”對應的節點其實沒有發生變化,只是在鏈表結構中的指標發生了變化,因此對應的迭代器訪問的時候,我們讀取位置不一樣。
因為我們可以知道我們LRU的節點肯定是在header的after指向節點。當我們重寫removeEldestEntry來修改LRU策略的時候,我們也是首先逐出header的after所指向的節點。然後,我們最後來看LinkedHashmap的迭代器的實現如下:
private abstract class LinkedHashIterator<T> implements Iterator<T> { Entry<K,V> nextEntry = header.after; Entry<K,V> lastReturned = null; /** * The modCount value that the iterator believes that the backing * List should have. If this expectation is violated, the iterator * has detected concurrent modification. */ int expectedModCount = modCount; public boolean hasNext() { return nextEntry != header; } public void remove() { if (lastReturned == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); LinkedHashMap.this.remove(lastReturned.key); lastReturned = null; expectedModCount = modCount; } Entry<K,V> nextEntry() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (nextEntry == header) throw new NoSuchElementException(); Entry<K,V> e = lastReturned = nextEntry; nextEntry = e.after; //此處我們可知我們的迭代器實現訪問節點的after引用指向的節點。 return e; } } 從LinkedHashMap實現的實現的迭代器內部類LinkedHashIterator看,我們迭代器的元素訪問順序是從header節點開始往after方向開始迭代。以上為對LinkedHashMap的分析簡單分析,下一節我們會針對LinkedHashMap的資料結構的特性來說說,用LinkedHashMap結構實現緩衝的快速存取的優勢,還會結合常用的開源實現,來說說實現一定並發的Android下緩衝實現。