概述
简介
LruCache是Android 3.1的时候出现的,一般我们为了兼容低版本会使用v4包下的。LruCache是一种缓存策略,持有的是强引用,但是会控制在一个峰值下。它内部维护了一个队列,每当从中取出一个值时,该值就移动到队列的头部。当缓存已满而继续添加时,会将队列尾部的值移除,方便GC。LruCache用于内存缓存,在避免程序发生OOM和提高执行效率有着良好表现。
LRU算法
和名字一样,LruCache的实现正是基于LRU(Least Recently Used)算法。最近最少使用,我理解的就是最久远的最少使用先被淘汰。下图展示了LRU算法的核心思想,是最常用也是比较简单的一种:
假设一个队列的最大容量是5,那么新进的元素会被添加到头部,当队列已满时继续添加会移除尾部的元素。值得注意的是,如果有一个不在队头的元素C又一次插入到队列,因为队列中已经存在C,则不会重复插入,而是将C元素移动到头部,相当于它的存在优先级当前是最高的。
LinkedHashMap
查看LruCache的源码很容易就发现,只有一个容器类,就是LinkedHashMap。正好LinkedHashMap是一个双向链表的数据结构,分为访问顺序和插入顺序。而LruCache只提供了一个有参构造函数:
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
在这里初始化了LinkedHashMap,并且是按访问顺序排序。也就是说,遍历该队列的entrySet,最先输出的总是最早被插入并且最近没有被访问(操作)过的。最后被访问的元素一定是作为队尾输出的。要注意这里的队头队尾,和LruCache描述的队列以及上图中的队列的区别,比较容易让人蒙蔽。
LruCache的初始化
以下是比较重要的几个成员变量及代表的含义:
private int size;
//当前缓存的大小
private int maxSize;
//最大可缓存的大小
private int hitCount;
//命中缓存的次数
private int missCount;
//丢失缓存的次数
LruCache的构造函数中传入参数为可缓存的最大容量并赋值给maxSize,如果在初始化该LruCache对象时没有重写其sizeOf
方法,那么maxSize就代表了其内部的LinkedHashMap可以存储的最大键值对数量。因为sizeOf
默认返回1,代表生产了一个键值对。注意只有maxSize和sizeOf
返回值是同一个单位制缓存的判断才有意义。
put
LruCache插入元素(缓存值)的方法:
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 += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
- 异常判断说明LruCache不允许键或值为null的操作。
- 在插入元素前会调用一次
sizeOf
,前面已经说过默认返回1,但一般我们会根据实际需要重写。比如用LruCache存储的value为File,那么sizeOf
返回的就应该是当前对应该key的文件大小。 - 相应的size也要完成自增长,因为当前缓存增加了,并且将对应的key-value插入到链表中。
- 二次检查,如果该key已经存在链表中,此时新的value覆盖后,size要减去之前的value所占用的大小。
- 上面的操作都是同步的,为了在多线程场景下保证size的准确性,否则缓存策略失效
- 如果是覆盖了旧的value,LruCache对外提供了一个空方法
entryRemoved
。 - 调用
trimToSize
,保证缓存不溢出。
trimToSize
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);
}
}
该方法每插入一次元素就会被调用一次。整个方法就是一个无限循环,判断当前缓存大小不大于最大容量则结束循环。否则就取出LinkedHashMap的entrySet的头部,也就是最早被插入且最近未被访问过的键值对并删除,更新size。重复此步骤直到缓存<=最大容量。不得不说利用访问顺序的LinkedHashMap的特性完成LRU缓存,非常巧妙。
get
public final V get(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;
}
}
- key不可以为null
- 如果map中存在与key相对应的value,则返回该value,并且缓存命中数+1。不存在,则缓存丢失数+1
- 不存在的话会尝试根据该key创建一个value。创建方法默认返回null,需要自己实现。
其它
除此之外,LruCache还提供了手动清除指定缓存remove(K key)
,清除所有缓存evictAll()
等方法供使用者调用。弄明白LinkedHashMap就很容易弄懂LruCache的实现了。
最后
以上就是可爱菠萝为你收集整理的LruCache——解决OOM的利器的全部内容,希望文章能够帮你解决LruCache——解决OOM的利器所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复