1、它是什么?
它是数据存储容器,可用于做数据缓存载体,优先淘汰那些近期最少使用的数据。LRU的全称为Least Recently Used。
2、如何使用?
2.1 简单使用
以kotlin代码为例,简洁使用如下
var lruCache: LruCache<Int, String>? = null
val maxCacheSize = 8
fun main() {
lruCache = LruCache(maxCacheSize)
for (index in 0..10) {
lruCache?.put(index, "$index")
}
print("\n===end===")
}
输出如下,可以看到,最早加入LruCache的数据被淘汰了
2.2 覆写sizeOf方法
上述例子中,存储对象的大小默认为1,实际使用场景中,往往需要传入内存大小,这时候需要覆写 sizeOf 方法,告知LruCache每个存储对象的大小。
var imgCache: LruCache<Int, Bitmap>? = null
var maxImgCacheSize: Int = 3072
@Test
fun test() {
imgCache = object : LruCache<Int, Bitmap>(maxImgCacheSize) {
override fun sizeOf(key: Int, value: Bitmap): Int {
if (value == null) {
return 0
}
return value.width * value.height
}
}
var tempBm50 = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888)
imgCache?.put(0, tempBm50)
var tempBm40 = Bitmap.createBitmap(40, 40, Bitmap.Config.ARGB_8888)
imgCache?.put(0, tempBm40)
print("\n===end===")
}
输出如下,可以看到,加入宽高为40的bitmap时,宽高为50的bitmap被淘汰了
3、谁用了它?而它又用了谁?
3.1 谁用了它?
Glide中的内存缓存就是通过LruCache实现的,具体的实现类为LruResourceCache。类似缓存策略的还有DiskLruCache。
3.2 它用了谁?
LruCache的代码量不算多,内部持有LinkedHashMap。
4、看看原理
看类结构不难发现,LruCache直接继承自Object,核心属性为LinkedHashMap类型的map。
4.1 LinkedHashMap
继承自HashMap,实现了Map接口
4.1.1 HashMap结构图
HashMap为数组和链表组合的数据结构,在jdk1.8版本后,当链表长度大于8时,转换为红黑树存储。优点是执行效率高,如查询、插入等操作。
4.1.2 LinkedHashMap结构图
LinkedHashMap继承自HashMap,可以直观地看到,基本结构还是数组与单向链表地结合,区别在于所有元素还通过双向链表关联,值得注意的是内部持有属性head、tail,分别表示双向链表表头、表尾,方便检索。
4.1.3 添加元素
调用put方法,即父类HashMap的 put 方法,即调用 putVal 方法,其中有个关键方法 afterNodeAccess ,LinkedHashMap重写了这个方法
afterNodeAccess 方法,这个方法的作用是将结点移动至双向链表尾部,属性tail被重新赋值,当LruCache检索最近使用的元素时可以方便获取。
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;
}
}
4.1.4 获取元素
调用 get 方法,即调用父类HashMap的 get 方法,可以看到当 accessOrder 为 true 时,仍然会调用 afterNodeAccess 方法,accessOrder 是何时被设置的呢?
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;
}
在LruCache的构造方法中,accessOrder 被置为了 true。
4.1.5 删除元素
调用 remove 方法,即HashMap的 remove 方法,即 removeNode 方法,内部调用LinkedHashMap重写的 afterNodeRemoval 方法。
afterNodeRemoval 方法,此方法比较简单,解除双向链表的关联,同时移动head或tail的指向。
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;
}
4.1.6 扩容
即HashMap的 resize 方法,当超过容器设定的阈值时,一般扩容至原来的两倍。
4.2 LruCache的添加元素
调用 put 方法,内部除了调用 LinkedHashMap的put方法外,还调用了sizeOf方法,用于计算当前元素的大小。不覆写 sizeOf 方法的话,元素大小默认记为 1。
protected int sizeOf(@NonNull K key, @NonNull V value) {
return 1;
}
最后调用 trimToSize 方法,判断是否需要淘汰最少使用的元素并执行淘汰算法。当有元素被淘汰时,回调 entryRemoved 方法供调用者感知,需覆写该方法。
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!");
}
if (size <= maxSize || map.isEmpty()) {
break;
}
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
4.3 LruCache获取元素
一般情况下还是会调用LinkedHashMap的 get 方法,值得注意的是,当在LinkedHashMap找不到key对应的value时,LruCache会尝试自造一个null元素结点与key对应,,最后将元素结点加入到LinkedHashMap中。
public final V get(@NonNull K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
/*
* Attempt to create a value. This may take a long time, and the map
* may be different when create() returns. If a conflicting value was
* added to the map while create() was working, we leave that value in
* the map and release the created value.
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
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;
}
}
4.4 LruCache删除元素
调用 remove 方法,同样的,回调 entryRemoved 方法可供调用者感知元素的淘汰情况。
public final V remove(@NonNull 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;
}
5、适用场景
适合需要LRU缓存策略的场景,如建立View缓冲池、图片资源缓冲池等等。
图中的调用者可以是ImageView、RecyclerView、Fragment等等,有了LruCache对源数据进行缓存过滤,保证调用者能立即使用到最常用的数据,提升程序效率。
6、注意事项
6.1 执行java的main方法
本文的代码示例使用androidTest进行的测试,如果要在java代码中直接进行测试,需要做特殊配置。如果是java文件,需要注意AndroidStudio中运行java文件的main方法时,需要配置 gradle.xml 文件,额外注意 <option name="delegatedBuild" value="false" /> 配置。当然,也可用通过Tools->Groovy Console运行。
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="delegatedBuild" value="false" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
<option name="testRunner" value="PLATFORM" />
</GradleProjectSettings>
</option>
</component>
</project>
6.2 多线程场景
LruCache中考虑了多线程的场景,如果单独使用LinkedHashMap,需要注意多线程操作的场景,HashMap在多线程中可能存在的问题LinkedHashMap中同样存在。