LruCache理解

        代码是基于Android 26,不同的版本可能会稍微有些不一样。LruCache的实现原理是基于LinkedHashMap,而LinkedHashMap是继承了HashMap并且实现了链表结构,所以首先要了解HashMap的实现和链表结构。

           HashMap的实现:HashMap内部数据存储是基于数组结构,如下:

transient Node<K,V>[] table;

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;  //key的hash值,这个对象保存到数组中的索引值(hash & (table.length-1))
    final K key;     //保存的key值
    V value;         //保存的value
    Node<K,V> next;  //当key的hash值相同时,保存在上一个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的put方法:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

        我们看到,put只是简单调用了putVal方法,hash(key)只是调用了key的hashCode方法,让我们来欣赏一下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;
    //根据hash和table的长度来确定Node存放的索引,如果该位置为null,则直接新建Node对象并存放在该位置
    //如果不为null,则将该位置的Node对象赋值给p
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //判断key的hash值和key值是否相等(hash值相等key值不一定相等,如ab和ba),
        //如果相等,则将该位置的value值替换并返回老的value
        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 {
            //遍Node(p)对象下的p.next,如果遍历到hash和key相等的节点就替换,
            // 没有遍历到就在最后一个节点上新增一个
            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
            //找到对应的key值,替换value
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //这是一个空方法,在LinkedHashMap中有实现,主要是添加到链表的尾部,后面会讲到
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    //LinkedHashMap中有实现,主要是实现在什么样的条件下可以删除最近最少使用的元素,后面会讲到
    afterNodeInsertion(evict);
    return null;
}

从这个方法中我们知道,从HashMap中获取值是不需要遍历整个数组的,直接通过((table.length-1) & hash)得到数组下标获取到Node对象,因此根据key值查询很快。

        这里说一下遍历HashMap的优化,通过keySet()拿到value的效率要比entrySet()要低一些。keySet()和entrySet()内部拿到实现都是拿到Node对象,只不过keySet()返回的是key,而entrySet()返回的是Node对象,如果是通过jeySet()去拿到value就有点多此一举了。

        链表结构:其实链表结构还是挺简单的,Java中的LinkedList用的就是链表结构,接下来我们看一下LinkedList保存的数据结构:

private static class Node<E> {
    E item;         //保存的对象
    Node<E> next;   //这个对象的前一个
    Node<E> prev;   //这个对象的后一个

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

在LinkedList中保存了一头一尾的两个对象,通过这两个对象,我们就可以从前或是从后遍历这个集合中的所有数据。

        接下来就让我们看一看LinkedHashMap的实现,首先看一下它的构造函数:

public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {
    super(initialCapacity, loadFactor);
    //这个参数很重要,默认是为false,意思就是说会保持我们的插入顺序不会变化
    //当为true时,每当通过get获取数据时,get到的数据会从该位置移到尾部
    this.accessOrder = accessOrder;
}

主要还是调用了HashMap的构造方法,HashMap的构造函数也只是初始化了数组需要的一些参数,然后就是给accessOrder赋值。通过上面对HashMap的分析,我们知道,主要的逻辑还是在put中。

        首先我们还是看一下LinkedHashMap存的数据结构:

static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
    LinkedHashMapEntry<K,V> before, after;
    LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

可以发现只是多了一前一后的引用,其实就是链表结构。接下来我们分析一下afterNodeAccess(Node<k,v> e)这个函数,

这个函数不仅在put中有用到,同时在get中也用到了,让我们看一下LinkedHashMap的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(Node<k,v> e)这个函数是如何将传进来的这个对象放置到尾部了。

       

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

 可以看到移动到尾部只是改变Node对象before和after的引用,这也是利用了链表的特性,可以说效率还是很高的。

        再来看一下HashMap中put中afterNodeInsertion(boolean evict)的实现,这个主要还是实现自定义条件删除最老的元素,上代码:

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMapEntry<K,V> first;
    //自定义删除的条件主要是在removeEldestEntry(first)实现,这个方法默认返回是false
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        //这个方法就是根据key删除最老的元素,里面的实现和put方法类似
        // 最后还会调用到afterNodeRemoval(Node<K,V> e)
        removeNode(hash(key), key, null, false, true);
    }
}

再来看一下afterNodeRemoval(Node<K,V> e)这个方法:

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

这个方法就是移除e元素,如果e元素是head元素,就将head后面的元素定义为head元素。

        至此,LinkedHashMap分析的就差不多了。

        接下来就来看看LruCache这个类的构造方法:

public LruCache(int maxSize) {
    if (maxSize <= 0) {
        throw new IllegalArgumentException("maxSize <= 0");
    }
    //这个变量根据sizeOf(K key, V value)返回值的不同,意义也是不一样的这个,当返回值为1时,代表map中最多只能存放maxSize个对象,
    //当size返回的的bitmap的大小时,maxSize代表的是缓存bitmap的最大值,即最大缓存内存
    //这也就是为什么我们图片缓存时要重写sizeOf(K key, V value)
    this.maxSize = maxSize;
    this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
protected int sizeOf(K key, V value) {
    return 1;
}
就是初始化缓存容器并给maxSize赋值,需要注意,这里将accessOrder设置为了true,说明我们获取元素时会将获取到的元素放置到尾部。接着让我们来瞧一瞧它的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++;
        //safeSizeOf()里面调用的就是sizeOf(),所以size就是根据sizeOf来计算大小的
        size += safeSizeOf(key, value);
        previous = map.put(key, value);
        //如果放置的这个这个key已经存在,size就需要减去替换掉的value的大小
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }

    if (previous != null) {
        //这是个空方法,当我们需要对替换掉的对象操作时就可以在这里做处理
        entryRemoved(false, key, previous, value);
    }
    //这个方法就是对设置的最大值和上面计算的size做比较,如果超过了最大值,
    // 就会将LinkedHashMap中的head(也就是最近最少使用的)删除
    trimToSize(maxSize);
    return previous;
}

        再来瞧一瞧tramToSize()是如何删除最近最少使用的对象的:

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作比较,如果size>maxSize就要删除最老的的元素
            if (size <= maxSize) {
                break;
            }
            //返回的就是head,也就是最老的那个元素
            Map.Entry<K, V> toEvict = map.eldest();
            if (toEvict == null) {
                break;
            }
            key = toEvict.getKey();
            value = toEvict.getValue();
            //删除最老的那个元素,内部调用的还是removeNode(hash(key), key, null, false, true)
            map.remove(key);
            //减去删除元素的大小
            size -= safeSizeOf(key, value);
            evictionCount++;
        }

        entryRemoved(true, key, value, null);
    }
}

        总结:

            1、LruCache内部维护就是一个LinkedHashMap;

            2、LinkedHashMap的数据结构由两部分,数组和链表,利用了数组查询快和链表增删快的特点;


猜你喜欢

转载自blog.csdn.net/tangedegushi/article/details/79851704