Android LruCache源码分析

1,154 阅读8分钟

Android设备由于内存大小限制,对于一些资源的缓存不能无限的添加,需要限制它的大小,那么当它达到最大限制时,如何选择要被移除出去的对象呢,Android sdk中为我们提供了一个LruCache的类,它可以让我们很方便的管理缓存。

概述

它是一个持有有限数量的值的缓存池,当一个值被访问到了,它就会被移动到队列的头部,当一个值被添加到一个装满了的缓存池,那么队列尾部的值将会被剔除出去并且可以被垃圾回收期回收。

这个类不允许null作为key或者value,如果get,put,remove方法返回了一个null,那就很明确的表明了这个key不存在。

这个类其实很简单,只有三百多行代码,它内部由一个LinkedHashMap来实现数据存储,其主要逻辑也是在LinkedHashMap中实现的,那么我们先来看一下LinkedHashMap。

LinkedHashMap

LinkedHashMap也是一个比较简单的类,它继承自HashMap,数据存储也跟HashMap一致,但是它内部维护了一个双向链表来记录所有节点,下面来看源码:

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

put

我们发现它并没有重写put方法, 那么它是如何将node节点连接起来的呢,还需要在HashMap的put中找线索。

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

final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                               int h, K k, V v) {
    ...
        TreeNode<K,V> xp = p;
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            Node<K,V> xpn = xp.next;
            //创建新的树节点
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            xp.next = x;
            x.parent = x.prev = xp;
            if (xpn != null)
                ((TreeNode<K,V>)xpn).prev = x;
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    ...
}

我们可以发现put过程中调用了创建节点的方法,而LinkHashMap中重写了这两个方法

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

TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
    TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
    linkNodeLast(p);
    return p;
}

//将p放入链表尾部
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
    LinkedHashMapEntry<K,V> last = tail;
    tail = p;//tail赋值为p
    //说明是第一个元素
    if (last == null)
        head = p;
    else {//将p的与之前的tail链接
        p.before = last;
        last.after = p;
    }
}

我们发现在创建节点的时候将p插入链表的尾部,其中head与tail是两个全局变量,表示链表的头节点与尾节点,我们还注意到HashMap的put方法最后一步执行了afterNodeInsertion()方法,这个方法在HashMap中是空实现,但是在LinkedHashMap中实现了

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    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) {
    return false;
}

方法中移除了链表中的第一个元素,但是removeEldestEntry在这里默认返回的null,也就是这里不做处理,如果你想在添加一个元素后,移除头部的元素,只需要实现removeEldestEntry方法,在满足移除条件的时候返回true即可,通常用于缓存。

get

LinkedHashMap重写了get方法,它与HashMap唯一的不同就是当accessOrder为true是调用了afterNodeAccess方法。

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

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

而afterNodeAccess方法就是把当前获取的节点放到队列的尾部。

remove

LinkedHashMap同样没有重写该方法,但是它实现了afterNodeRemoval,而HashMap的removeNode的方法最后也会调用这个方法

void afterNodeRemoval(Node<K,V> e) { // unlink
    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;
}

这个方法就是讲当前要移除的节点断开与链表的链接。

LruCache

下面开始看LruCache

 /**
  * @param maxSize for caches that do not override {@link #sizeOf}, this is
  *     the maximum number of entries in the cache. For all other caches,
  *     this is the maximum sum of the sizes of the entries in this cache.
  */
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构造方法接收一个maxSize作为缓存的最大值的参数,如果我们没有重写sizeOf方法,那么它就代表元素的个数。

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
        size += safeSizeOf(key, value);
        //放入集合
        previous = map.put(key, value);
        if (previous != null) {
            //如果previous不为null,表示只是更新了值,此时还需要减去previous的大小
            size -= safeSizeOf(key, previous);
        }
    }

    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }
	//检查size是否超出
    trimToSize(maxSize);
    return previous;
}

private int safeSizeOf(K key, V value) {
    int result = sizeOf(key, value);
    if (result < 0) {
        throw new IllegalStateException("Negative size: " + key + "=" + value);
    }
    return result;
}

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

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!");
            }
            //如果size没有超出,跳出循环
            if (size <= maxSize) {
                break;
            }
            //否则,找到map中最老的元素,返回的是链表的头部
            Map.Entry<K, V> toEvict = map.eldest();
            if (toEvict == null) {
                break;
            }
            key = toEvict.getKey();
            value = toEvict.getValue();
            //移除
            map.remove(key);
            //更新size
            size -= safeSizeOf(key, value);
            evictionCount++;
        }
        entryRemoved(true, key, value, null);
    }
}

LinkedHashMap:
public Map.Entry<K, V> eldest() {
    return head;
}

put时如果原来key对应的value不为空,调用了entryRemoved,这里是个空实现,如果我们保存的资源需要手动来释放的话可以重写该方法来释放资源,还有sizeOf默认返回1,也就是元素的个数,如果我们希望限制的元素占用内存的大小,比如Bitmap限制为4M,那就应该这样写,sizeOf返回bitmap的大小做累加,entryRemoved释放bitmap

int cacheSize = 4 * 1024 * 1024; // 4MiB
LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
     protected int sizeOf(String key, Bitmap value) {
        return value.getByteCount();
     }
    
    protected void entryRemoved(boolean evicted, K key, Bitmap oldValue, Bitmap newValue) {
        oldValue.recycle();
    }
}}

get

通过put方法的分析,get就比较好理解了


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

    V mapValue;
    synchronized (this) {
        //正常返回,注意这里调用了LinkedHashMap的get方法后,将当前节点插入到链表的尾部
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }
	//如果没有key,则创建一个key对应的value,默认实现是null
    V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }

    synchronized (this) {
        createCount++;
        //将新创建的值放入map
        mapValue = map.put(key, createdValue);
        //正常情况下应该不会出现这种情况,应该是在并发时,如果一个线程create时另外一个线程进入该同步块put元素后会出现这种情况
        if (mapValue != null) {
            // There was a conflict so undo that last put
            map.put(key, mapValue);
        } else {
            //size增加,safeSizeOf调用了sizeOf,sizeOf默认返回1,也就是元素的数量
            size += safeSizeOf(key, createdValue);
        }
    }
	//如果maoValuw不为null,size没有发生变化,直接返回
    if (mapValue != null) {
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
    } else {//否则检查size是否超过最大允许的大小
        trimToSize(maxSize);
        return createdValue;
    }
}

remove

remove也比较简单,不过多介绍

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

    if (previous != null) {
        entryRemoved(false, key, previous, null);
    }

    return previous;
}

总结

LruCache的大致流程,当put一个键值对的时候,通过LinkedHashMap将该节点放到链表的末端,当get一个key的时候,如果存在,则将该节点插入链表末端,而当size超出maxSize时,删除头部节点,也就是说当一个key对应的节点频繁被访问,或者刚刚被访问过,那么它一定是在链表的后面的,所以删除的时候就不会删除这个节点,而如果一个key对用的节点很长时间不访问到时它在头部,那么一旦size超限,它就会被删除,这就是LRU的实现方式:最近最少使用。

所以源码中对类的描述:每当一个value被访问时,它就会被移动到队列的头部,这个说法的方向应该是跟LinkedHashMap中双向链表的方向是相反的。