LruCache 解析

419 阅读4分钟
原文链接: wusp.github.io

Basics

LruCache一种在Framework和App中都经常会被使用到的工具类,特别是在很多App的图片处理框架中。其目标指在提供一个Latest Recently Used的Key/Value 缓存工具,并为经常访问到的元素提高更高的访问效率和查询速度
其工作模式有以下几个特点:

  • 每当缓存中的元素被访问到时就将该元素移动到List的尾部,从而保证在访问经常使用的元素时有更好的访问效率,也天然有着对元素被访问频率的筛选
  • 当缓存size满了之后自动将长时间不使用的元素移除,不会因为Cache不断增大而导致OOM。
  • public API的内部都使用了 synchronized 来加锁,保证线程安全。
  • 提供元素的“生命周期回调”函数。
  • 提供了关键函数的复写,轻易的满足开发者在使用数据结构过程中的定制化需求。create()定义了如何创建一个新的Value对象;由于缓存的可能是各种不同类型的对象,对占据缓存空间的度量方法可能不一样,提供sizeOf()的复写,该方法指示了该如何度量一对Key/Value占据的缓存空间。。

底层数据结构原理

LruCache的内部存储使用的是LinkedHashMap的数据结构,同时该Map的初始化容量为0Map增长因子为0.75链表顺序为根据access调整。而Latest Recently Used的功能本质是在LinkedHashMap中实现的。
LinkedHashMap在实现LRU功能是通过 put - get 操作配对使用来实现的。

HashMap.put

LruCache的put最先进到HashMap的put()中:

@Override public V put(K key, V value) {
if (key == null) {
return putValueForNullKey(value);
}
int hash = secondaryHash(key.hashCode());
//如果存在则替换原来的值。
HashMapEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);
for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
//将该元素移动到List的队尾(最近使用的放队尾)。
preModify(e);
V oldValue = e.value;
e.value = value;
return oldValue;
}
}

// 不存在,创建一个新的,并调用LinkedHashMap的addNewEntry添加进队列中。
modCount++;
if (size++ > threshold) {
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
addNewEntry(key, value, hash, index);
return null;
}

LinkedHashMap.addNewEntry()

@Override void addNewEntry(K key, V value, int hash, int index) {
LinkedEntry<K, V> header = this.header;

// Remove eldest entry if instructed to do so.
LinkedEntry<K, V> eldest = header.nxt;
if (eldest != header && removeEldestEntry(eldest)) {
remove(eldest.key);
}

// 放置到List的尾部(最近使用)
LinkedEntry<K, V> oldTail = header.prv;
LinkedEntry<K, V> newTail = new LinkedEntry<K,V>(
key, value, hash, table[index], header, oldTail);
//很关键!!这里将最近使用的header.prv 指针保存到了hash table中
//后面查询时是从hash table中的指针开始查起,也就是header.prv最近使用的元素
table[index] = oldTail.nxt = header.prv = newTail;
}

看完put,看看get是怎么配对实现的:

LinkedHashMap.get

LruCache中的get,直接调用了LinkedHashMap中的get():

@Override 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.
*/
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
//说明不允许空的元素存在
if (e == null)
return null;
if (accessOrder)
//加入到List的尾部
makeTail((LinkedEntry<K, V>) e);
return e.value;
}

// Doug Lea's supplemental secondaryHash function (inlined)
int hash = key.hashCode();
hash ^= (hash >>> 20) ^ (hash >>> 12);
hash ^= (hash >>> 7) ^ (hash >>> 4);

HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
//从List头开始查,访问到了则移动到List尾部(在LruCache使用时accessOrder = true)
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
}
return null;
}

一句话实现LRU的核心:每次操作过后都将使用到的元素移动到List尾部(List在hash table中的队头);查询时先从List的尾部开始查起。

操作API

由于LRU的真正实现是在底层的LinkedHashpMap当中,因此操作的API都相对简单。

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的增长同sizeOf的定义有关
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
//本次插入导致有Value被移除,size根据sizeOf的定义减小。
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
//回调Key/Value的生命周期函数
entryRemoved(false, key, previous, value);
}
//确保LruCache中保存的元素不会超出限制。
trimToSize(maxSize);
return previous;
}

控制和更新size;回调生命周期函数;往LinkedHashMap中添加元素。

get
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//从LinkedHashMap中取,
//更新一下统计
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
//使用create()方法根据指定的Key创建一个新的Value对象
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
//如果冲突发生,则移除create()创建的这个Value对象。
if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}

get方法也简单,跟put方法也有很多共通之处。需要注意的一点是:如果该Key在Cache中不存在,则根据create()方法创建一个新的Value对象并同Key一起保存到缓存当中。 由于create()方法可能需要较长的时间执行,race的情况下很可能当create()返回之后这个Key已经被插入到Cache当中。这个时候会从Cache过一遍,但是确保create()创建的这个新对象不会添加到Cache当中。