Android LruCacha 源码分析

来源:互联网 发布:怎么查看淘宝小号满月 编辑:程序博客网 时间:2024/06/14 17:20

LRU 算法,中文叫 近期最少使用 算法。这个算法的思想是当容量已经满的情况下,我们再次向容器里面存放东西的时候,我们需要先删除之前使用最少的元素,因为这个元素在新元素插入之间一使用的最少,我们认为在后面的过程中,这个元素依然是使用次数最少的,当容积不够的时候,我们当然要删除后面过程中使用次数最少的,腾出空间给新的元素。

Android 里面实现了这种算法,名字叫 LruCache 。下面我们从源码(android sdk 23)的角度来解析这个类

public class LruCache<K, V> {    private final LinkedHashMap<K, V> map; // 最重要的一个变量,上面所说的原理就是利用这个 LinkedHashMap 实现的    /** Size of this cache in units. Not necessarily the number of elements. */    private int size; // 已经缓存的元素个数    private int maxSize; // 缓存大小    private int putCount; // 往里面添加元素计数器    private int createCount;// key 为 null 的情况下,默认生成对象的个数    private int evictionCount; // 当加入的元素超过 maxSize 后,调整大小的次数    private int hitCount; // 命中的次数    private int missCount; // 没有命中的次数    /**     * @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);    }    // 省略其他代码}

我们先来看看 put 方法是如何工作的

public final V put(K key, V value) {    // 如果 key 或 value 为 null 那么抛出异常,说明不接受 null    if (key == null || value == null) {        throw new NullPointerException("key == null || value == null");    }    V previous; // 记录 key 的旧值    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 是空        entryRemoved(false, key, previous, value);    }    // 调整大小,也是最重要的方法之一    trimToSize(maxSize);    // 返回之前插入的值    return previous;}

刚才提到了 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!");            }            // size(缓存的元素个数) 小于 maxSize(容量) 那么跳出循环            if (size <= maxSize) {                break;            }            // eldest() 取 LinkedHashMap 里面最年老的元素            Map.Entry<K, V> toEvict = map.eldest();            if (toEvict == null) { // 为空说明 LinkedHashMap 里面没有元素了                break;            }            key = toEvict.getKey();            value = toEvict.getValue();            // 删掉 最近最少使用的            map.remove(key);            size -= safeSizeOf(key, value); // 修正 size            evictionCount++; // 缩减计数器        }        // 删除元素,entryRemoved 是一个空方法        entryRemoved(true, key, value, null);    }}

trimToSize() 方法里面 LinkedHashMap.eldest() 方法很重要,LinkedHashMap.eldest() 的作用就是取出最年老,即最近最少使用的元素。如何取到最近最少使用的元素就是 LinkedHashMap 里面的东西了。我们接下来看看 LinkedHashMap 的源码

public class LinkedHashMap<K, V> extends HashMap<K, V> {    /**     * A dummy entry in the circular linked list of entries in the map.     * The first real entry is header.nxt, and the last is header.prv.     * If the map is empty, header.nxt == header && header.prv == header.     */    transient LinkedEntry<K, V> header;    /**     * True if access ordered, false if insertion ordered.     */    private final boolean accessOrder;    /**     * LinkedEntry adds nxt/prv double-links to plain HashMapEntry.     */    static class LinkedEntry<K, V> extends HashMapEntry<K, V> {        LinkedEntry<K, V> nxt;        LinkedEntry<K, V> prv;        /** Create the header entry */        LinkedEntry() {            super(null, null, 0, null);            nxt = prv = this;        }        /** Create a normal entry */        LinkedEntry(K key, V value, int hash, HashMapEntry<K, V> next,                    LinkedEntry<K, V> nxt, LinkedEntry<K, V> prv) {            super(key, value, hash, next);            this.nxt = nxt;            this.prv = prv;        }    }    // 省略其他代码}

首先,有一个 LinkedEntry 内部类,继承自 HashMapEntry , 里面有 2 个引用分别指向之前和之后的元素,说明这个是一个双向链表结构。 LinkedEntry<K, V> header 这个变量就是双向链表的表头了。 accessOrder 这个的意思是是否需要排序,这个在后面会用到。我们先看看 eldest() 方法。

public Entry<K, V> eldest() {    LinkedEntry<K, V> eldest = header.nxt;    return eldest != header ? eldest : null;}

方法很简单,就是取出 header 的 nxt 元素。那为什么 header 的 nxt 袁术就是最近最少使用的元素呢?这个是因为在 putget 过程中,如果 accessOrder 设置为 true 的话,会把对应 key 的 LinkedEntry 节点放到队列的尾部。下面我们通过 get 源码分析下这个过程。

public V get(Object key) {    /*     * This method is overridden to eliminate the need for a polymorphic     * invocation in superclass at the expense of code duplication.     */    // 如果 key 为空,做key 为空的特殊处理    if (key == null) {        HashMapEntry<K, V> e = entryForNullKey;        if (e == null)            return null;        if (accessOrder)            makeTail((LinkedEntry<K, V>) e);        return e.value;    }    // 计算 hash 值    int hash = Collections.secondaryHash(key);    HashMapEntry<K, V>[] tab = table;    // 遍历,找到 key 对应的元素节点    for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];            e != null; e = e.next) {        K eKey = e.key;        if (eKey == key || (e.hash == hash && key.equals(eKey))) {            if (accessOrder) // accessOrder 如果为 true ,那么就执行将元素节点放到队列尾部的操作                makeTail((LinkedEntry<K, V>) e);            return e.value;        }    }    // 如果这个 key 之前没有 put 过,那么就返回 null.    return null;}

看代码发现, accessOrdertrue 的时候,我们才会去做将元素节点放到队列的操作,而在 LruCache 里面,LinkedHashMap 的这个值是 true 的。整个的 LruCache 分析就到这里为止。
最后我们看下 makeTail 的源码

private void makeTail(LinkedEntry<K, V> e) {    // Unlink e 断开 e 的链接    e.prv.nxt = e.nxt;    e.nxt.prv = e.prv;    // Relink e as tail 将 e 链接到队列尾部。所以队列头部就是最早put进去但是一直没使用的    // 也就是所谓的 "最近最少使用"    LinkedEntry<K, V> header = this.header;    LinkedEntry<K, V> oldTail = header.prv;    e.nxt = header;    e.prv = oldTail;    oldTail.nxt = header.prv = e;    modCount++;}
0 0
原创粉丝点击