LruCache源碼解析

前言

本篇將介紹LruCache,而LruCache是用LinkedHashMap實現的,LinkedHashMap繼承HashMap所以沒看過HashMap的先看下我另外篇博文HashMap源碼解析(JDK8)再來看本篇。

接下來是正菜LruCache不過吃之前我們先看下前菜LinkedHashMap,只要LinkedHashMap弄明白了LruCache也就小菜一碟了,本文的LinkedHashMap是基於JDK1.8的。

概述

LinkedHashMap繼承至HashMap大部分方法都直接沿用,不同點如下。

  1. 相對於HashMap的節點增加了首尾指針形成了一個雙向鏈表,並在put()插入的時候按先後順序新添加的節點插入到鏈表的尾部remove()刪除的時候移除該節點保證鏈表的順序。
  2. 新增accessOrder參數,除了按插入順序維護鏈表外,還可以按訪問順序,將操作過的節點移動到尾部方便實現我們的近期最少使用算法。
  3. 內部是有序的,迭代的時候按鏈表順序從前往後迭代。

LruCache內部使用LinkedHashMap實現,在構造方法的時候會讓我們傳入能存儲的最大容量maxSize,在新增元素的時候會調用trimToSize()方法,方法內會將sizeOf()方法計算得到的每個元素的大小的總和size與構造方法傳入的maxSize比較,如果大於maxSize則刪除最舊的元素,即LinkedHashMap的頭部元素,直到size<=maxSize

正文

先來LinkedHashMap源碼,再來LruCache的。還是按照構造方法、增、刪、改、查、迭代器的順序介紹。

LinkedHashMap

首先我們要知道的是LinkedHashMapHashMap的子類,大部分方法都是直接沿用HashMap的。

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

構造方法

    final boolean accessOrder;//是否按照訪問順序維護鏈表順序 默認是false按照插入順序維護
    transient LinkedHashMapEntry<K,V> head;//雙向鏈表的頭結點
    transient LinkedHashMapEntry<K,V> tail;//雙向鏈表的尾結點
	public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

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

    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }

    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

    static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {//繼承HashMap.Node並給每個結點新增before, after指針
        LinkedHashMapEntry<K,V> before, after;
        LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

總結

  1. 構造方法和HashMap差不多,就多了一個我們可以自己設置accessOrder值的。
  2. 新增headtail字段,方便獲取鏈表的頭尾。
  3. 繼承HashMap.Node並給每個結點增加首尾指針,方便順序的維護。

增、改

LinkedHashMap並沒有自己實現put()方法還是沿用父類HashMap的,最終會調到putVal()方法,這個方法在HashMap源碼解析(JDK8)已經講過了,這裏我們直接看LinkedHashMap中的不同處。

    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);//在插入新元素的時候會調用newNode()方法創建新元素
        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);//在插入新元素的時候會調用newNode()方法創建新元素
                        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);//如果存在相同key的鍵值對並且替換了舊的值則調用afterNodeAccess()方法
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);//如果插入了新的元素則調用afterNodeInsertion()方法
        return null;
    }

接下來看插入新元素會調用的兩個方法newNode()afterNodeInsertion()

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

    void afterNodeInsertion(boolean evict) { //插入新節點通知
        LinkedHashMapEntry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {//判斷是否要刪除舊的節點
            K key = first.key;
            removeNode(hash(key), key, null, false, true);//移除節點
        }
    }

    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {//默認返回false不會刪除舊的元素
        return false;
    }

從源碼可以看出newNode()是創建新節點插入到鏈表的尾部,afterNodeInsertion()是插入新節點的通知方法,裏面會通過evict標記和removeEldestEntry()方法判斷是否要移除舊的節點,默認實現是不移除。

再來看覆蓋value的通知afterNodeAccess()方法

    void afterNodeAccess(Node<K,V> e) { //移動節點到尾部
        LinkedHashMapEntry<K,V> last;
        if (accessOrder && (last = tail) != e) {//如果accessOrder爲true並且覆蓋的元素不是最後一個
            LinkedHashMapEntry<K,V> p =
                (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
            p.after = null;//將元素e的尾指針置爲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 {//將e添加到鏈表尾部
                p.before = last;
                last.after = p;
            }
            tail = p;//尾指針指向e
            ++modCount;
        }
    }

如果我們accessOrder爲true並且覆蓋的節點不是最後一個節點,則將他移動到鏈表的尾部。

總結下增和改的操作

  1. 如果不存在key相同的節點則通過newNode()創建新節點插入到鏈表的尾部,然後調用afterNodeInsertion()方法通知有新的節點插入,裏面會通過evict標記和removeEldestEntry()方法判斷是否要移除舊的節點,默認實現是不移除。
  2. 如果存在相同key的節點,則默認覆蓋該節點的值,然後調用afterNodeAccess()方法通知有節點被覆蓋,如果accessOrder爲true並且覆蓋的節點不是最後一個節點,則將他移動到鏈表的尾部。

LinkedHashMap刪除還是使用的HashMap的方法,只不過在刪除的時候調用了afterNodeRemoval()方法通知,那我們直接看該方法的實現。

    void afterNodeRemoval(Node<K,V> e) {
        LinkedHashMapEntry<K,V> p =
            (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
        p.before = p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

可以看到就是將要刪除的節點前後指針置爲null,然後把前後元素的指針調整下。

這個方法是LinkedHashMap自己的,不過內部還是調用的HashMapgetNode()方法獲取節點

    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)//調用父類的getNode()獲取節點
            return null;
        if (accessOrder)//如果accessOrder爲true
            afterNodeAccess(e);//將訪問的元素移動到尾部
        return e.value;
    }

可以發現如果accessOrder爲true則調用afterNodeAccess()方法,這個方法在前面增的時候說過會將傳入的節點放到鏈表的尾部。

迭代

    public Set<Map.Entry<K,V>> entrySet() {//獲取entrySet
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
    }

    final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {//entrySet對象
       	...
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new LinkedEntryIterator();
        }
        ...
    }

    final class LinkedEntryIterator extends LinkedHashIterator
        implements Iterator<Map.Entry<K,V>> {//迭代器對象
        public final Map.Entry<K,V> next() { return nextNode(); }
    }
    
    abstract class LinkedHashIterator {
        LinkedHashMapEntry<K,V> next;
        LinkedHashMapEntry<K,V> current;
        int expectedModCount;

        LinkedHashIterator() {
            next = head;//從頭部開始迭代
            expectedModCount = modCount;
            current = null;
        }

        public final boolean hasNext() {
            return next != null;
        }

        final LinkedHashMapEntry<K,V> nextNode() {//迭代方法 按鏈表順序從前往後迭代
            LinkedHashMapEntry<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            current = e;
            next = e.after;
            return e;
        }

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

通過實現可以發現LinkedHashMap是有序的,迭代是按鏈表插入順序從前往後迭代。

總結

這裏對LinkedHashMap做一個總結

  1. LinkedHashMap內部維護了一個雙向鏈表,保證了LinkedHashMap是一個有序的哈希表
  2. 在構造方法的時候可以將accessOrder置爲true,使LinkedHashMap按訪問順序維護鏈表的順序
  3. 迭代效率較高,直接從鏈表的頭部到尾部依次迭代。

LruCache

看了LinkedHashMap再來看LruCache就非常簡單了。

構造方法

    private int size;//當前lrucache大小 
	private int maxSize;//最大容量
	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);//使用LinkedHashMap存儲元素,並且將accessOrder置爲了true
    }

在構造方法的時候我們傳入該LruCache最大的容量maxSize,然後初始化LinkedHashMap作爲容器存儲元素並且將accessOrder置爲了true。再來看一個計算每個元素size的關鍵方法sizeOf()

    protected int sizeOf(K key, V value) {
        return 1;
    }

這個方法需要我們自己實現,用來計算每個添加進LruCache的元素的size。並且這個size的單位和構造方法傳入的maxSize單位要保持一致,因爲後面會用這兩個值進行比對判斷是否要移除舊的元素。

增、改

    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);//計算添加元素大小,添加進size變量
            previous = map.put(key, value);//添加到LinkedHashMap
            if (previous != null) {//如果之前有添加過
                size -= safeSizeOf(key, previous);//減去之前元素的size
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, value);//通知有元素移除
        }

        trimToSize(maxSize);//判斷是否超出maxSize要移除舊的元素
        return previous;
    }

    private int safeSizeOf(K key, V value) {//計算每個元素的size
        int result = sizeOf(key, value);//實際調用sizeOf獲取元素大小
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
    }

    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) {//判斷size是否超過maxSize
                    break;
                }

                Map.Entry<K, V> toEvict = map.eldest();//拿到LinkedHashMap最舊的元素即頭部
                if (toEvict == null) {
                    break;
                }

                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);//移除
                size -= safeSizeOf(key, value);//減去移除元素的size
                evictionCount++;
            }

            entryRemoved(true, key, value, null);//通知有元素移除
        }
    }

    public Map.Entry<K, V> eldest() {//LinkedHashMap中最舊的元素head
        return head;
    }

增加和修改的元素先通過sizeOf()方法計算要加入元素大小添加到size字段,然後把元素添加進map,如果之前有key相同的元素則size減去之前元素的大小,然後調用trimToSize()方法判斷size是否大於了maxSize如果大於了,則刪除map中最舊的元素直到size小於等於maxSize

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

        V previous;
        synchronized (this) {
            previous = map.remove(key);//移除節點
            if (previous != null) {
                size -= safeSizeOf(key, previous);//減去移除節點的size
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, null);//通知有元素移除
        }

        return previous;
    }

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

        V createdValue = create(key);//獲取到的mapValue爲null則調用create()方法,create()默認返回的null
        if (createdValue == null) {
            return null;
        }

        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);//將create()方法生成的值插入map

            if (mapValue != null) {//插入位置原先有值
                map.put(key, mapValue);//那麼不插入我們create()方法生成的值
            } else {//原先位置沒值
                size += safeSizeOf(key, createdValue);//計算插入值的大小賦值給size
            }
        }

        if (mapValue != null) {//原先位置有值
            entryRemoved(false, key, createdValue, mapValue);//通知createdValue插入失敗
            return mapValue;
        } else {//插入了新的值
            trimToSize(maxSize);//判斷是否要刪除舊的元素
            return createdValue;
        }
    }

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

一般情況下就是直接通過LinkedHashMapget()方法直接查詢元素。特殊情況如果沒找到key對應的元素並且實現了create()方法,判斷key對應位置是否有元素,如果沒有則插入create()方法創建的元素,將元素大小添加到size,然後調用trimToSize(maxSize)調整判斷是否要刪除舊的元素。

總結

這裏對LruCache做一個總結

  1. 內部通過LinkedHashMap實現,maxSizesizeOf()方法返回值單位要一致。
  2. 每次添加元素的時候通過sizeOf()方法計算每個元素的大小,當size大於maxSize的時候會刪除LinkedHashMap中最舊的元素即頭部,直到size<=maxSize
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章