Android LruCache源码分析

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

概述

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

这个类不容许null做为key或者value,若是get,put,remove方法返回了一个null,那就很明确的代表了这个key不存在。缓存

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

LinkedHashMap

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

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{
	...
}
复制代码

put

咱们发现它并无重写put方法, 那么它是如何将node节点链接起来的呢,还须要在HashMap的put中找线索。ide

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中重写了这两个方法this

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中实现了spa

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方法。code

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中双向链表的方向是相反的。

相关文章
相关标签/搜索