-
LRU原理
LRU
全称为Least Recently Used
,即最近最少使用,是一种缓存置换算法。大家都知道在各个图片加载的第三方框架中都有他的身影。在各个加载工具流行之前,这个算法还是比较常用的。当然现在不用我们手写了。每个框架都封装的很好。我们前面分析glide的框架,他的缓存机制也是由lru缓存和软引用组成的。在这里先介绍一下lrucache, 既然是一种缓存策略,那么肯定不会无限制的增加,当内存大到某个阈值的时候,会舍弃一部分保存在内存里面的数据,舍弃规则就是最近最少未使用的被舍弃。比如:A,C,D,E,D,A,C,D.如果这个时候内存已满需要舍弃一个。这个时候舍弃的是E。因为它最长时间未被使用。
-
使用方法
public class LruCacheUtils{
//首先计算分给缓存的内存大小,一般是app可用内存的1/8;
long maxSize = Runtime.getRuntime().maxMemory()/8;
//初始化LruCache
LruCache<String,Bitmap> lruCache = new LruCache<String,Bitmap>((int)maxSize){
//重写sizeOf方法,返回存储一个对象需要占用的内存 本例存储的是bitmap,所以返回的是bitmap大
//小。这里有一个细节需要注意,sizeof返回的单位必须和maxsize的单位保持一致。否则会造成错误
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
}
以上是简单的Lrucache的使用,只是重写了一个必须的方法sizeOf,接下来慢慢扩充。在我们使用的过程中,我们其实可以大致算出可以缓存多少图片,一般app的1/8是4m。然后我们可以根据图片的大小然后猜测可以缓存的图片张数,现在很多app都是高清图片,这个时候也许只能缓存几张,这个时候我们需要想办法压缩图片来增加缓存的数量。否则图片过大很容易oom。
-
源码分析
首先我们看看Lrucache的结构和成员变量。
public class LruCache<K, V> {
//存储数据的LinkedHashMap, 其实lrucache就是对它的一个具体应用
private final LinkedHashMap<K, V> map;
/** Size of this cache in units. Not necessarily the number of elements. */
private int size; //当前缓存的大小
private int maxSize; //可用的最大缓存大小
private int putCount; //有效的put方法的使用次数。key,value任意为空均无效
private int createCount; //create方法调用次数
private int evictionCount;//被置换出的元素个数
private int hitCount;
private int missCount;
/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is
* the maximum number of entries in the cache. For all other caches,
* this is the maximum sum of the sizes of the entries in this cache.
*/
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);
}
其实Lrucache就是对LinkedHashMap的一个具体使用,都是对源码中map的操作, 在构造函数中初始化map,其中两个变量需要注意,一个是0.75f, 一个是最后的boolean变量, 我们可以通过看LinkedHashmap的源码来理解。
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
这个0.75是一个负载因子,与LinkedHaspMap的扩容有关系,0.75表示当LinkedHashMap中数组的长度大于最大长度的0.75倍的时候就需要扩容,这个负载因子过大过小都不合适,下一篇博客会专门讲LinkedHashMap的源码,再次不细说了。知道概念即可。最后的一个布尔变量当它为false
时,只按插入的顺序排序,即新放入的顺序会在链表的尾部;而当它为true
时,更新或访问某个节点的数据时,这个对应的结点也会被放到尾部。这个也是实现lru算法的根本。
接下来我们看看Lrucache如何取数据:
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
//首先加锁是为了实现线程安全
synchronized (this) {
//这里只是一句但是包含2个内容,首先是获取key对应的值,其次是hashmap会重新
//整理存储的数据的顺序,可在hashmap中查看这个内容。
mapValue = map.get(key);
if (mapValue != null) {
//如果mapvalue不为空,那么表示以前有过存储直接返回。
hitCount++;
return mapValue;
}
missCount++;
}
//如果根据key没有获取到数据,我们可以自定义生产一个对象放回。如果不重写create函数
//那么就create直接返回null,这个时候整个函数也直接返回null。如果我们重写create,
//这个生成的过程也许比较长,在这个期间如果对应的key有了存储数据,那么后面就会产生冲突
//当发生冲突的时候,会舍弃我们自己生成的这个value,保存原来的值。因为是多线程的所以会
//发生这样的事情。
V createdValue = create(key);
if (createdValue == null) {
return null;
}
/* 如果不复写create函数,以下不会执行*/
synchronized (this) {
createCount++;
//将生成的值保存到map
mapValue = map.put(key, createdValue);
if (mapValue != null) {
//如果返回的值不为空,那么说明key已经保存了值,也就是和我们自己生成的值
//产生了冲突,这个时候需要保存原来的值,舍弃我们生成的。
map.put(key, mapValue);
} else {
//保存成功之后,需要计算保存的元素的内存大小,然后加到size上面,获取当前
//使用内存的情况。
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
/*
* 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了
* 告诉 自定义 的 entryRemoved 方法
*/
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
//这里因为之前的size+=,所以需要重新整理长度。
trimToSize(maxSize);
return createdValue;
}
}
只是看lrucache的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;
}
因为我们设置的accessOrder是true,所以我们需要看afterNodeAccess函数
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;
}
}
这个函数将更新过的数据移动到了双向链表的末尾。
接下来我们看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 += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
//如果之前的数据不为空,那么需要再减去以前保存数据的大小
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
//当这个key之前保存过数据,那么需要通知更新保存的数据。
//在这里一般是置空,我们可以通过复写这个函数来实现我们自己的二级缓存。
//不过这个二级缓存不能使用内存的缓存了。可以写到sd卡上面。
entryRemoved(false, key, previous, value);
}
//需要重新整理内存情况
trimToSize(maxSize);
return previous;
}
这里的整个逻辑分以下几步:
1 先把数据保存到map里面,所以不管这个时候是不是超过了分配的最大内存。
2 计算存储之后的内存只用情况,该加的就加,需要再减的也要把废弃的data减去。
3 如果此前存储的对象不为空,那么通知处理需要移除的数据。
4 计算之后的整体内存情况。
接下来我们看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) {
break;
}
Map.Entry<K, V> toEvict = map.eldest();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
这里通过一个死循环来确保缓存使用的内存大小,小于初始化的最大值,当size大于maxsize的时候,就不停的移除map中的数据在计算知道size小于maxsize。或者map为空。
所以最后我们可以稍微扩充一下Lrucache
//初始化LruCache
LruCache<String,Bitmap> lruCache = new LruCache<String,Bitmap>((int)maxSize){
//重写sizeOf方法,返回存储一个对象需要占用的内存 本例存储的是bitmap,所以返回的是bitmap大小。
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
//处理被置换出的元素
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
//此方法下可以做一个二级缓存的策略。把置换出的数据保存到本地。然后下次加载的时候,可以现在本地查找。之后在去缓存中查找。也可以在这里释放内存,释放掉被置换出的数据。
}
};
-
总结
1 Lrucache根本上是通过Linkedhashmap来实现了他的思想。当添加数据导致内存溢出的时候,map会把链表头部数据舍弃,就是舍弃 了最近最久未使用的元素。
2 Lrucache是线程安全的。
3 其实可以看出Lrucache中并没有释放内存,只是将LinkedHashMap中的数据移除。 可以通过在entryRemoved中释放。