lrucache,最近最少使用缓存策略,源码其实很简单,没有多少行。下面我们分两个部分来解析:
第一部分:如何使用
/**
* 存储的key类型
* 存储的value类型
* 设置最大存储容量
* 计算每个存储内容大小
*/
LruCache<String, Bitmap> bitmapLruCache=new LruCache<String, Bitmap>(1024*1024){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
public void useLruCache(){
//保存数据
bitmapLruCache.put("111",bitmap);
//获取数据
Bitmap bitmap = bitmapLruCache.get("111");
if(null!=bitmap){
//使用数据
}
}
第二部分:源码分析
首先我们先来翻译一下LruCache源码头文件内容:
* A cache that holds strong references to a limited number of values. Each time
* a value is accessed, it is moved to the head of a queue. When a value is
* added to a full cache, the value at the end of that queue is evicted and may
* become eligible for garbage collection.
//这个类是一个强引用的有一定大小限制的缓存类。每次访问这个存储的内容,他都将被放到队列头部
//(这里应该是表述错误,应该是放到队列尾部).如果一个内容想要被添加进一个容量已经满的缓存中
//那么处于缓存队列尾部的内容将会被移除队列,并且被垃圾回收器回收。
*
* <p>If your cached values hold resources that need to be explicitly released,
* override {@link #entryRemoved}.
//如果缓存持有的内容需要被释放掉,那么需要重写entryRemoved方法。
*
* <p>If a cache miss should be computed on demand for the corresponding keys,
* override {@link #create}. This simplifies the calling code, allowing it to
* assume a value will always be returned, even when there's a cache miss.
//如果一个被移除的缓存要求重新回到缓存队列中,那么可以重新create方法。这个方法,允许一个
//将要被回收的内容被重新恢复。
*
* <p>By default, the cache size is measured in the number of entries. Override
* {@link #sizeOf} to size the cache in different units. For example, this cache
* is limited to 4MiB of bitmaps:
//通常情况下,缓存内容的大小根据缓存条目数量来衡量。重新sizeOf方法,可以改变计算缓存内容的方
//法。例如,缓存内容控制在4M以内的缓存bitmap的实例:
* <pre> {@code
* int cacheSize = 4 * 1024 * 1024; // 4MiB
* LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
* protected int sizeOf(String key, Bitmap value) {
* return value.getByteCount();
* }
* }}</pre>
*
* <p>This class is thread-safe. Perform multiple cache operations atomically by
* synchronizing on the cache: <pre> {@code
* synchronized (cache) {
* if (cache.get(key) == null) {
* cache.put(key, value);
* }
* }}</pre>
* //这个类是线程安全的。
* <p>This class does not allow null to be used as a key or value. A return
* value of null from {@link #get}, {@link #put} or {@link #remove} is
* unambiguous: the key was not in the cache.
//这个类不允许存储的key或者value为null。如果以key保存的内容不存在,那么get put remove方法
//将会返回null
通过头文件,我们可以知道:
1.lrucache是线程安全的,关键操作都是枷锁的
2.不允许key或者value为null去缓存数据
3.sizeof方法应该说是必须要重写的,用来计算存储内容大小。如果被缓存的内容移除时需要释放掉,那么entryRemoved 方法中可以释放。当然create方法可以让那些被释放的内容起死回生。
4.上面给出了一个如何使用的示例。
源码分析:
1.创建
/**
* @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) {
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
设置最大缓存容量,初始化LinkedHashMap,双向链表保存数据,特别注意最后一个参数是true,表示如果链表节点每次被访问则会重新排序,将访问的节点放入到链表尾部。
2.存储数据
public final V put(K key, V value) {
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;
}
计算存储内容大小,放入链表中,如果之前链表中有内容,则计算新内容和老内容大小差值,并且调用entryRemoved方法,我们可以在这个方法中释放旧内容。计算缓存内容是否超过设定最大值。trimToSize 方法我们看一下:
private void trimToSize(int maxSize) {
//只要目前缓存内容大小超过最大值,则不停的删除头节点
while (true) {
K key;
V value;
synchronized (this) {
//如果内容大小小于最大值,则退出循环
if (size <= maxSize) {
break;
}
//获取头节点
Map.Entry<K, V> toEvict = map.eldest();
if (toEvict == null) {
break;
}
// END LAYOUTLIB CHANGE
if (toEvict == null) {
break;
}
//删除头结点
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
只要目前存储的内容大于设定的最大值,则将链表的头节点数据删除掉,直到小于设置的最大值。
3.获取数据:
public final V get(K key) {
V mapValue;
synchronized (this) {
//获取内容,同时将此节点放到到链表尾部
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
}
}
此方法会获取到节点内容,同时会将节点重新排序,放入到链表尾部。
我们可以看到,lrucache充分利用了linkhashmap源码只要节点被访问则会重新排序,将访问节点放到最后节点的特点。
Lrucache源码分析完了,但是貌似没有说过他的缺点是什么,好像都是优点,那么他的缺点是什么呢?lrucache有一个致命的缺点,那就是被他引用的对象都是强引用,强引用特点就是宁可jvm挂掉,也不释放对象,所以lrucache反而会增大内存消耗,特别是当内存紧张的时候,系统需要释放一些内存来保命时,但是对于lrucache他没有任何办法?那怎么解决呢?我们把他持有的对象换成软引用不就可以了吗?用法如下:
LruCache<String, SoftReference<String>> lruCache=new LruCache<String, SoftReference<String>>(1000){
@Override
protected int sizeOf(String key, SoftReference<String> value) {
if(value.get()!=null){
return value.get().length();
}
return 0;
}
}
好像是完美解决了,当内存不足时,被软引用持有的对象就会被回收了。但是,但是,仔细想想,lrucache是用来干什么的?保存最近最多使用对象的,如果你内存不足了,就把我对象给回收掉了,那我的意义在哪呢?本来我A对象刚使用了,结果你给我回收了,是不是跟我的设计初衷不符合了呢?而且还面临着,被回收了之后我就需要重新计算目前占用内存大小问题。所以说,这个方案不可取。那怎么完美解决呢?参考glide源码解决,glide是将三级缓存扩充到了四级,第一级lrucache,第二级弱引用+引用计数法,第三级本地缓存,第四级才是联网。当我们获取图片时,会先从lrucache中获取,如果有,则将图片从lrucache中remove掉,同时添加到弱引用中,同时弱引用中有个计算器,引用数量+1,下次再去lrucache取时,肯定就没有了,会去弱引用中找,引用数量再+1,当不用的时候,引用数量-1,直到为0,则重新将弱引用对象放入到lrucache中。也就是说,lrucache只持有最近使用的,当前正在使用的则用弱引用持有,那么如果内存紧张了,则可以直接将lrucache清空释放掉内存,这就完美解决了这个致命问题。同时,也说明glide实际使用的最大内存比lrucache申请的内存要大,因为正在使用的图片内存没计算在内。