代码是基于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的数据结构由两部分,数组和链表,利用了数组查询快和链表增删快的特点;