1. 程式人生 > >Android面試題(22)-lruCache與DiskLruCache快取詳解

Android面試題(22)-lruCache與DiskLruCache快取詳解

關於lruCache(最近最少使用)的演算法,這是一個比較重要的演算法,它的應用非常廣泛,不僅僅在Android中使用,Linux系統等其他地方中也有使用;今天就來看一看這其中的奧祕;

講到LruCache,就不得不講一講LinkedHashMap,而對於LinkedHashMap,它繼承的是HashMap,那麼我們就先從HashMap開始看起吧;

注:此篇部落格所講的所有知識都是在jdk1.8環境下的,java8的hashmap相比之前的版本又做了一層優化,當連結串列過長時(預設超過8),會改為採用紅黑樹這種自平衡的資料結構去進行儲存優化

HashMap

我們知道,資料結構中的存在兩種常見的儲存結構,一個是陣列,一個是連結串列;兩者各有優劣,首先陣列的儲存空間在記憶體中是連續的,這就就導致佔用記憶體嚴重,連續的大記憶體進入老年代的可能性也會變大,但是正因為如此,定址就顯得簡單,也就是說查詢某個arr會有指定的下標,但是插入和刪除比較困難,因為每次插入和刪除時,如果陣列在插入這個地方後面還有很多資料,那就要後面的資料整體往前或者往後移動。對於連結串列

來說儲存空間是不連續的,佔用記憶體比較寬鬆,它的基本結構是一個節點(node)都會包含下一個節點的資訊(如果是雙向連結串列會存在兩個資訊一個指向上一個一個指向下一個),正因為如此定址就會變得比較困難,插入和刪除就顯得容易,連結串列插入和刪除的時候只需要修改節點指向資訊就可以了。

那麼兩者各有優劣,將它們兩者結合起來會有什麼效果呢?自然早就有大神嘗試過了,並且嘗試的很成功,它的產物就是HashMap雜湊表,也叫散列表;

HashMap的主幹是一個數組,裡面儲存的是一個個的Node,Node中包含了雜湊值,key,value和下一個Node的引用;

Node(int hash, K key, V value, 
Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }
儲存在HashMap中的每一個值都需要一個key,這是為什麼呢?這個問題可以再問細一點,hashmap是如何存放資料的?

我們先來看看他的一些基本屬性:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

這個屬性表示HashMap的初始容量大小是16;

static final int MAXIMUM_CAPACITY 
= 1 << 30;

最大容量為2^30;

static final float DEFAULT_LOAD_FACTOR = 0.75f;

這個表示載入因子預設為0.75,代表hashmap的填充程度,載入因子越大,填滿的元素越多,好處是,空間利用率高了,但:衝突的機會加大了.連結串列長度會越來越長,查詢效率降低。

反之,載入因子越小,填滿的元素越少,好處是:衝突的機會減小了,但:空間浪費多了.表中的資料將過於稀疏(很多空間還沒用,就開始擴容了)

衝突的機會越大,則查詢的成本越高.

因此,必須在 "衝突的機會"與"空間利用率"之間尋找一種平衡與折衷. 這種平衡與折衷本質上是資料結構中有名的"時-空"矛盾的平衡與折衷.

  如果機器記憶體足夠,並且想要提高查詢速度的話可以將載入因子設定小一點;相反如果機器記憶體緊張,並且對查詢速度沒有什麼要求的話可以將載入因子設定大一點。不過一般我們都不用去設定它,讓它預設為0.75就可以了;

/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2 and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 */
static final int TREEIFY_THRESHOLD = 8;
/**
 * The bin count threshold for untreeifying a (split) bin during a
 * resize operation. Should be less than TREEIFY_THRESHOLD, and at
 * most 6 to mesh with shrinkage detection under removal.
 */
static final int UNTREEIFY_THRESHOLD = 6;
臨界值,這個欄位主要是用於當HashMap的size大於它的時候,需要觸發resize()方法進行擴容

構造方法:

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

可以清晰的看到當new一個HashMap時,並沒有為陣列分配記憶體空間(有一個傳入map引數的構造方法除外);

幾個核心方法:

put方法實際呼叫的就是putVal方法,所以我們先看putVal方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
                    break;
}
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
p = e;
}
        }
        if (e != null) { // existing mapping for key
V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
afterNodeAccess(e);
            return oldValue;
}
    }
    ++modCount;
    if (++size > threshold)
        resize();
afterNodeInsertion(evict);
    return null;
}

這裡的邏輯由於是java8,所以會複雜一點,裡面有幾個關鍵點,記錄下來,對比著原始碼看:

(1)putVal方法其實就可以理解為put方法,我們使用hashmap的時候,什麼時候才會使用put方法呢,當你想要儲存資料的時候會呼叫,那麼putVal方法的邏輯就是為了把你需要儲存的資料按位置存放好就可以了;

(2)具體的存放邏輯是通過複雜的if判斷來完成的,首先會判斷當前通過key和hash函式計算出的陣列下標位置的是否為null,如果是空,直接將Node物件存進去;如果不為空,那麼就將key值與桶中的Node的key一一比較,在比較的過程中,如果桶中的物件是由紅黑樹構造而來,那麼就使用紅黑樹的方法去進行儲存,如果不是,那麼就繼續判斷當前桶中的元素是否大於8,大於8的話就使用紅黑樹處理(呼叫treeifybin方法),如果小於8,那麼進行最後的判斷是否key值相同,如果相同,就直接將舊的node物件替換為新的node物件;這樣就保證了儲存的正確性;

(3)在putVal中有這麼一句

++modCount;

這裡的modCount的作用是用來判斷當前HashMap是否在由一個執行緒操作,因為hashmap本身是執行緒不安全的,多執行緒操作會造成其中資料不安全等多種問題,modcount記錄的是put的次數,如果modcount不等於put的node的個數的話,就代表有多個執行緒同時操作,就會報ConcurrentModificationException異常;

再來看看get方法,get方法其實呼叫的是getNode方法

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
} while ((e = e.next) != null);
}
    }
    return null;
}

這裡也有幾個點:

  1. bucket裡的第一個節點,直接命中;
  2. 如果有衝突,則通過key.equals(k)去查詢對應的entry 
    若為樹,則在樹中通過key.equals(k)查詢,O(logn); 
    若為連結串列,則在連結串列中通過key.equals(k)查詢,O(n)。

接下來看看hashmap中邏輯最複雜但是也最為經典的擴容機制,他主要是由resize方法實現的:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
}
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
}
    else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
}
    threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
loTail.next = e;
loTail = e;
}
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
hiTail.next = e;
hiTail = e;
}
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
newTab[j] = loHead;
}
                    if (hiTail != null) {
                        hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
                }
            }
        }
    }
    return newTab;
}

說到擴容,就不得不提到上述的幾個屬性

(1)Capacity:hashmap的容量,其實就是hashmap陣列的長度,也就是capacity=array.length

(2)threshold:擴容的臨界值,當陣列中元素的個數達到這個值的時候,就會進行擴容

(3)loadFactor:載入因子,表示陣列的填充程度,預設為0.75(不要輕易修改)

這三者的關係是threshold/loadFactor=Capacity;

resize方法中主要是做了如何去擴容的邏輯判斷,其中包括

(1)如果此時hashmap的容量大於2^30,那麼就不擴容,不擴容的方法是將threshold的值賦值為2^30-1,就不會擴容了

(2)一次擴容的大小是擴容一倍,如果初始大小為16,那麼擴容後為32

(3)Java8的hashmap由於引入了紅黑樹,所以如果採用桶內的儲存結構為紅黑樹的話,那麼會呼叫相應紅黑樹的演算法,如果是連結串列,那麼就會將連結串列拆分為兩個連結串列,再將兩個連結串列重新放入相對應的的位置中,這裡是需要重新計算每個元素的hash值的,因為要保證,舊的陣列和新的陣列的元素的索引要保證相同;

到這裡,有幾個問題要回答:

  1. 什麼時候會使用HashMap?他有什麼特點?

    是基於Map介面的實現,儲存鍵值對時,它可以接收null的鍵值,是非同步的,HashMap儲存著Entry(hash, key, value, next)物件。

  2. 你知道HashMap的工作原理嗎?

    通過hash的方法,通過put和get儲存和獲取物件。儲存物件時,我們將K/V傳給put方法時,它呼叫hashCode計算hash從而得到bucket位置,進一步儲存,HashMap會根據當前bucket的佔用情況自動調整容量(超過Load Facotr則resize為原來的2倍)。獲取物件時,我們將K傳給get,它呼叫hashCode計算hash從而得到bucket位置,並進一步呼叫equals()方法確定鍵值對。如果發生碰撞的時候,Hashmap通過連結串列將產生碰撞衝突的元素組織起來,在Java 8中,如果一個bucket中碰撞衝突的元素超過某個限制(預設是8),則使用紅黑樹來替換連結串列,從而提高速度。

  3. 你知道get和put的原理嗎?equals()和hashCode()的都有什麼作用?

    通過對key的hashCode()進行hashing,並計算下標( n-1 & hash),從而獲得buckets的位置。如果產生碰撞,則利用key.equals()方法去連結串列或樹中去查詢對應的節點

  4. 你知道hash的實現嗎?為什麼要這樣實現?

    在Java 1.8的實現中,是通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這麼做可以在bucket的n比較小的時候,也能保證考慮到高低bit都參與到hash的計算中,同時不會有太大的開銷。

  5. 如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?

    如果超過了負載因子(預設0.75),則會重新resize一個原來長度兩倍的HashMap,並且重新呼叫hash方法。 
    關於Java集合的小抄中是這樣描述的: 
    以Entry[]陣列實現的雜湊桶陣列,用Key的雜湊值取模桶陣列的大小可得到陣列下標。 
    插入元素時,如果兩條Key落在同一個桶(比如雜湊值1和17取模16後都屬於第一個雜湊桶),Entry用一個next屬性實現多個Entry以單向連結串列存放,後入桶的Entry將next指向桶當前的Entry。 
    查詢雜湊值為17的key時,先定位到第一個雜湊桶,然後以連結串列遍歷桶裡所有元素,逐個比較其key值。 
    當Entry數量達到桶數量的75%時(很多文章說使用的桶數量達到了75%,但看程式碼不是),會成倍擴容桶陣列,並重新分配所有原來的Entry,所以這裡也最好有個預估值。 
    取模用位運算(hash & (arrayLength-1))會比較快,所以陣列的大小永遠是2的N次方, 你隨便給一個初始值比如17會轉為32。預設第一次放入元素時的初始值是16。 
    iterator()時順著雜湊桶陣列來遍歷,看起來是個亂序。

  6. 當兩個物件的hashcode相同會發生什麼?

    因為hashcode相同,所以它們的bucket位置相同,‘碰撞’會發生。因為HashMap使用連結串列儲存物件,這個Entry(包含有鍵值對的Map.Entry物件)會儲存在連結串列中。

  7. 如果兩個鍵的hashcode相同,你如何獲取值物件?

    找到bucket位置之後,會呼叫keys.equals()方法去找到連結串列中正確的節點,最終找到要找的值物件。因此,設計HashMap的key型別時,如果使用不可變的、宣告作final的物件,並且採用合適的equals()和hashCode()方法的話,將會減少碰撞的發生,提高效率。不可變效能夠快取不同鍵的hashcode,這將提高整個獲取物件的速度,使用String,Interger這樣的wrapper類作為鍵是非常好的選擇

  8. 如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?

    預設的負載因子大小為0.75,也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)一樣,將會建立原來HashMap大小的兩倍的bucket陣列,來重新調整map的大小,並將原來的物件放入新的bucket陣列中。這個過程叫作rehashing,因為它呼叫hash方法找到新的bucket位置

  9. 你瞭解重新調整HashMap大小存在什麼問題嗎?

    當重新調整HashMap大小的時候,確實存在條件競爭,因為如果兩個執行緒都發現HashMap需要重新調整大小了,它們會同時試著調整大小。在調整大小的過程中,儲存在連結串列中的元素的次序會反過來,因為移動到新的bucket位置的時候,HashMap並不會將元素放在連結串列的尾部,而是放在頭部,這是為了避免尾部遍歷(tail traversing)。如果條件競爭發生了,那麼就死迴圈了。因此在併發環境下,我們使用CurrentHashMap來替代HashMap

  10. 為什麼String, Interger這樣的wrapper類適合作為鍵?

    因為String是不可變的,也是final的,而且已經重寫了equals()和hashCode()方法了。其他的wrapper類也有這個特點。不可變性是必要的,因為為了要計算hashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的hashcode的話,那麼就不能從HashMap中找到你想要的物件。不可變性還有其他的優點如執行緒安全。如果你可以僅僅通過將某個field宣告成final就能保證hashCode是不變的,那麼請這麼做吧。因為獲取物件的時候要用到equals()和hashCode()方法,那麼鍵物件正確的重寫這兩個方法是非常重要的。如果兩個不相等的物件返回不同的hashcode的話,那麼碰撞的機率就會小些,這樣就能提高HashMap的效能

這就是關於HashMap的解析,下面看看LinkedHashMap的原始碼解析,LinkedHashMap繼承自HashMap,所以理解了HashMap,LinkedHashMap就很簡單了;

LinkedHashMap

首先看一下他的繼承關係:

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

繼承HashMap,實現了Map介面

再看看他的成員變數

transient LinkedHashMapEntry<K,V> head;

用於指向雙向連結串列的頭部

transient LinkedHashMapEntry<K,V> tail;

用於指向雙向連結串列的尾部

final boolean accessOrder;

用於LinkedHashMap的迭代順序,true表示基於訪問的順序來排列,也就是說,最近訪問的Node放置在連結串列的尾部,false表示按照插入的順序來排列;

構造方法:

跟HashMap類似的構造方法這裡就不一一贅述了,裡面唯一的區別就是添加了前面提到的accessOrder,預設賦值為false——按照插入順序來排列,這裡主要說明一下不同的構造方法。

public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
accessOrder = false;
}

get()方法:

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

這裡的afterNodeAccess方法是按照訪問順序排列的關鍵:

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方法比hashmap就複雜了一些,因為他在得到值的同時,還需要將得到的元素放在連結串列的尾部,至於是怎麼放置的,無非就是資料結構中的雙向迴圈連結串列的知識,分四種情況:


  • 正常情況下:查詢的p在連結串列中間,那麼將p設定到末尾後,它原先的前節點b和後節點a就變成了前後節點。

  • 情況一:p為頭部,前一個節點b不存在,那麼考慮到p要放到最後面,則設定p的後一個節點a為head
  • 情況二:p為尾部,後一個節點a不存在,那麼考慮到統一操作,設定last為b
  • 情況三:p為連結串列裡的第一個節點,head=p

put方法:

在LinkedHashMap中是找不到put方法的,因為,它使用的是父類HashMap的put方法,不過它將hashmap中的put方法中呼叫的相關方法去重寫了,具體的就是newNode(),afterNodeAccess和afterNodeInsertion方法

先看newNode方法:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMapEntry<K,V> p =
        new LinkedHashMapEntry<K,V>(hash, key, value, e);
linkNodeLast(p);
    return p;
}
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
    LinkedHashMapEntry<K,V> last = tail;
tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
last.after = p;
}
}

主要功能就是把新加的元素新增到連結串列的尾部;

有關LinkedHashMap,因為與HashMap相似,我只提了裡面的儲存順序問題,這也是LinkedHashMap的最主要的功能;

LruCache記憶體快取原理

在講LruCache之前 ,先看看它是怎麼使用的,拿它在圖片快取的應用來說,看下面的程式碼:

private LruCache<String,Bitmap> lruCache;
public MemoryCacheUtils(){
    //獲取手機最大記憶體的1/8
long memory=Runtime.getRuntime().maxMemory()/8;
lruCache=new LruCache<String, Bitmap>((int)memory){
        @Override
protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount();
}
    };
}

/**
 * 從記憶體中讀圖片
 * @param url
* @return
*/
public  Bitmap getBitmapFromMemory(String url) {
    Bitmap bitmap = lruCache.get(url);
    return bitmap;
}

public void setBitmapToMemory(String url, Bitmap bitmap) {
    lruCache.put(url,bitmap);
}

這是最簡單的圖片的三級快取中的記憶體快取的寫法,我們先看使用構造器new一個LruCache發生了什麼:

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的三個引數的構造方法,保證按照訪問順序來排列元素,生成一個LinkedHashMap物件,賦值給map;

在看它的get方法

public final V get(K key) {
    if (key == null) {
        throw new NullPointerException("key == null");
}

    V mapValue;
    synchronized (this) {
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
}
        missCount++;
}

    /*
     * Attempt to create a value. This may take a long time, and the map
     * may be different when create() returns. If a conflicting value was
     * added to the map while create() was working, we leave that value in
     * the map and release the created value.
     */
V createdValue = create(key);
    if (createdValue == null) {
        return null;
}

    synchronized (this) {
        createCount++;
mapValue = map.put(key, createdValue);
        if (mapValue != null) {
            // There was a conflict so undo that last put
map.put(key, mapValue);
} else {
            size += safeSizeOf(key, createdValue);
}
    }

    if (mapValue != null) {
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
} else {
        trimToSize(maxSize);
        return createdValue;
}
}

這裡面主要做了兩件事,首先會根據key查詢map中是否存在對應的Value,也就是對應key值的快取,如果找到,直接命中,返回此份快取,如果沒有找到,會呼叫create()方法去嘗試建立一個Value,但是我看了create()原始碼,是返回null的;

protected V create(K key) {
    return null;
}

也就是說,如果你不主動重寫create方法,LruCache是不會幫你建立Value的;其實,正常情況下,不需要去重寫create方法的,因為一旦我們get不到快取,就應該去網路請求了;

再看put方法:

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;
}

主要邏輯是,計算新增加的大小,加入size,然後把key-value放入map中,如果是更新舊的資料(map.put(key, value)會返回之前的value),則減去舊資料的大小,並呼叫entryRemoved(false, key, previous, value)方法通知舊資料被更新為新的值,最後也是呼叫trimToSize(maxSize)修整快取的大小。

LruCache大致原始碼就是這樣,它對LRU演算法的實現主要是通過LinkedHashMap來完成。另外,使用LRU演算法,說明我們需要設定快取的最大大小,而快取物件的大小在不同的快取型別當中的計算方法是不同的,計算的方法通過protected int sizeOf(K key, V value)實現,我們要快取Bitmap物件,則需要重寫這個方法,並返回bitmap物件的所有畫素點所佔的記憶體大小之和。還有,LruCache在實現的時候考慮到了多執行緒的訪問問題,所以在對map進行更新時,都會加上同步鎖。

DiskLruCache硬碟快取原理

講完LruCache之後,我們趁熱打鐵,抓緊看一下DiskLruCache硬碟快取的相關原理,DiskLruCache和LruCache內部都是使用了LinkedHashMap去實現快取演算法的,只不過前者針對的是將快取存在本地,而後者是直接將快取存在記憶體;

先看看它是如何使用的吧,這裡和LruCache不一樣,DiskLruCache不在Android API內,所以如果我們要使用它,必須將其原始碼下載,可以點選這裡進行下載,下載完成後,匯入到你自己的專案中就可以使用了;

首先你要知道,DiskLruCache是不能new出例項的,如果我們要建立一個DiskLruCache的例項,則需要呼叫它的open()方法,介面如下所示:

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

open()方法接收四個引數,第一個引數指定的是資料的快取地址,第二個引數指定當前應用程式的版本號,第三個引數指定同一個key可以對應多少個快取檔案,基本都是傳1,第四個引數指定最多可以快取多少位元組的資料。

其中快取地址通常都會存放在 /sdcard/Android/data/<application package>/cache 這個路徑下面,但同時我們又需要考慮如果這個手機沒有SD卡,或者SD正好被移除了的情況,因此比較優秀的程式都會專門寫一個方法來獲取快取地址,如下所示:

public File getDiskCacheDir(Context context, String uniqueName) {
    String cachePath;
    if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) {
        cachePath = context.getExternalCacheDir().getPath();
} else {
        cachePath = context.getCacheDir().getPath();
}
    return new File(cachePath + File.separator + uniqueName);
}

可以看到,當SD卡存在或者SD卡不可被移除的時候,就呼叫getExternalCacheDir()方法來獲取快取路徑,否則就呼叫getCacheDir()方法來獲取快取路徑。前者獲取到的就是 /sdcard/Android/data/<application package>/cache 這個路徑,而後者獲取到的是 /data/data/<application package>/cache 這個路徑。

接著是應用程式版本號,我們可以使用如下程式碼簡單地獲取到當前應用程式的版本號:

public int getAppVersion(Context context) {
    try {
        PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
        return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
}
    return 1;
}

後面兩個引數就沒什麼需要解釋的了,第三個引數傳1,第四個引數通常傳入10M的大小就夠了,這個可以根據自身的情況進行調節。

因此,一個非常標準的open()方法就可以這樣寫:

private DiskLruCache getDiskLruCache(Context context){
    try {
        File cacheDir = getDiskCacheDir(context, "bitmap");
        if (!cacheDir.exists()) {
            cacheDir.mkdirs();
}
        mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
} catch (IOException e) {
        e.printStackTrace();
}
    return mDiskLruCache;
}

關於寫入快取:

寫入的操作是藉助DiskLruCache.Editor這個類完成的。類似地,這個類也是不能new的,需要呼叫DiskLruCache的edit()方法來獲取例項,介面如下所示:

public Editor edit(String key) throws IOException 
現在就可以這樣寫來得到一個DiskLruCache.Editor的例項:
public void setBitmapToLocal(Context context,String url, InputStream inputStream) {
    BufferedOutputStream out = 
            
           

相關推薦

Android試題22-lruCacheDiskLruCache快取

關於lruCache(最近最少使用)的演算法,這是一個比較重要的演算法,它的應用非常廣泛,不僅僅在Android中使用,Linux系統等其他地方中也有使用;今天就來看一看這其中的奧祕;講到LruCache,就不得不講一講LinkedHashMap,而對於LinkedHashM

Android試題23-圖片的三級快取工具類

上一篇部落格已經把三級快取原理大致都講了,這篇部落格就僅僅貼一下封裝好的一個圖片三級快取工具類,程式碼內有註釋,僅僅小記一下:首先是MyBitmapUtils,它提供了一個display方法去供外界呼叫:/** * 圖片三級快取工具類 * Created by PDD o

android 試題

程序 一個 如果 intent傳值 存儲 新的 有一個 數據類型 andro 1、Android中真實寬高,getWidth和getMeasuredWidth的區別:哪個計算的是真實的寬? getWidth():得到的是View在父Layout中布局好後的寬度值,如果沒有父

整理一些常見的java及android試題2

                1. 什麼是Activity?四大元件之一,一般的,一個使用者互動介面對應一個activity, activity 是Context的子類,同時實現了window.callback和keyevent.callback, 可以處理與窗體使用者互動的事件. 我開發常用的的有List

Android試題31-App啟動流程

先貼個連結,總結的挺全面 在看這篇文章之前,希望先看完我的之前的部落格 android面試(6)-Binder機制,因為關於App啟動流程設計很多Binder通訊; 先將“三個程序”,“六個大類”進行介紹: 三個程序: Launcher程序:整個App啟動流程的起點,

Android試題5

1. Android的自動恢復功能是什麼? 恢復備份設定和資料來重新安裝程式 2. Handler是執行緒與Activity通訊的橋樑,將任務執行緒放入佇列裡面派對執行;

Android試題27-android的事件分發機制

今天開始寫一點關於view的知識,先從最基本的講吧,android的事件分發機制,其實在我看來,android的事件分發機制在現實生活中經常能看到,所以我覺得還是很好理解的;先看看生活中常見的一種情形吧;比如說,現在你所在的公司中有一項任務被派發下來了,專案經理把專案交給你的

android試題2——Fragment篇

1、Fragment為什麼被稱為第五大元件 Fragment比Activity更節省記憶體,其切換模式也更加舒適,使用頻率不低於四大元件,且有自己的生命週期,而且必須依附於Activity 2、Activity建立Fragment的方式 靜態建立 動態建立 3、Fragme

android試題

自己總結了一些android的面試題,先寫一部分,後續在補充 一、Android的四大元件是哪些?它們的作用是? 答:Activity是android程式和使用者互動的介面,相當於單獨的螢幕,需要為保持各介面的狀態做很多持久化的事情,管理生命週期和一些邏輯跳轉。  

Android試題——Activity的生命週期和啟動模式

引言 這份面試題系列文章旨在查漏補缺,通過常見的面試題發現自己在Android基礎知識上的遺漏和欠缺,驗證所學是否紮實。 這是系列的第一章,後面我會根據安卓知識模組分類併網羅分析各種常見面試題。 面試題: Activity的生命週期 答

Android試題24-有關bitmap的操作

有關bitmap的操作一直很多,這裡特此總結一下:public class BitmapTransformUtils { //根據圖片uri生成Bitmap物件 public static Bitmap getBitmapByUrl(Context context,

Android試題28-android的view載入和繪製流程

View的載入流程view佈局一直貫穿於整個android應用中,不管是activity還是fragment都給我們提供了一個view依附的物件,關於view的載入我們在開發中一直使用,在接下來的幾篇文章中將介紹在android中的載入機制和繪製流程並且對於基於android

京東android試題2018 頂級網際網路公司試題系列

以下來自於北京的一個兄弟的面試題 1.靜態內部類和非靜態內部類有什麼區別   2.談談你對java多型的理解   3.如何開啟執行緒,run和runnable有什麼區別   4.執行緒池的好處   5.說一下你知道的設計模式有哪些,介紹下介面卡模式 &n

Android試題2018.11.16

一、UI的繪製過程,常見優化手段以及原理。 二、有幾種常見的單例模式?對於這幾種單例模式synchronized具體鎖的是什麼東西? 三、問記憶體優化你做過沒有?一張十萬乘以十萬的圖片,如何載入才不會記憶體溢位? 四、問記憶體溢位,記憶體抖動,記憶體洩漏你都碰到過嗎?怎麼解決的?如何區分

精選22道Spring Boot 試題

總結有關於spring boot的面試題,不看答案你是否全部都能答出來? 問題一 什麼是Spring Boot? 多年來,隨著新功能的增加,spring變得越來越複雜。只需訪問https://spring.io/projects頁面,我們就會看到可以在我們的應

演算法試題-- 統計學習模式識別試題

題目: 答案解析: 第一部分: 1.統計學習是關於計算機基於資料構建概率統計模型並運用模型對資料進行預測與分析的一門學科,又稱為統計機器學習; 特點:以計算機為平臺;以資料為物件;以方法為中心;以概率論、統計學、資訊理論以及最優化理論等為理論依託;目的是實現對資料的預測

Android試題文章內容來自他人部落格

騰訊面試題 1.int a = 1; int result = a+++3<<2; 2.int a = 2; int result = (a++ > 2)?(++a):(a+=3); 3.int a = 1234567; int b = 0x06;

【資料結構】棧佇列的試題

一.使用兩個佇列實現(實現棧先進後出的特點)     思路:              1.建立兩個佇列的結構體,並將這倆個佇列(Queue1和Queue2)的結構體封裝到一個結構體裡。                       2.入棧:判斷哪個佇列中為空(Queue1和

python試題

以及 args 空格 代碼實現 spa adding 技術分享 變量作用域 區別 Python中基本數據結構的操作 元組 列表 字典 集合 定義

每天五個java相關試題8--spring篇

ioc 簡單 組件 print 提交數據 常常 spring容器 效果 用戶 首先呢,假設有從事前端開發的大神或者準備從事前端開發的小夥伴無意看到我這篇博客看到這段文字歡迎加我的QQ:【 845415745 】。即將走入社會的菜鳥大學生有關於前端開發的職