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