简介
LruCache 主要用于缓存的处理。缓存可以缓解服务器压力,同时节省客户端流程和提高客户端性能。
关于Android的三级缓存,其中主要的就是内存缓存和硬盘缓存。这两种缓存机制的实现都应用到了Lru算法(Least Recently used )。
从android应用缓存数据的形式来区分有两种形式: 数据缓存 和图片缓存
- 数据缓存
主要用于保存业务的数据,如接口的返回数据 - 图片缓存
主要用于保存应用程序的图片对象,图片在程序里使用比较频繁,而且比较占有网络资源。
这两种形式是类同的,只是缓存的数据不一样,前者是文本内容,后者是图片内容。
LruCache 实现原理分析
LruCache中Lru算法的实现就是通过LinkedHashMap来实现的。LinkedHashMap继承于HashMap,它使用了一个双向链表来存储Map中的Entry顺序关系。这种顺序有两种:
一种是LRU顺序,一种是插入顺序,这可以由其构造函数public LinkedHashMap(int initialCapacity,float loadFactor, boolean accessOrder)指定。所以,对于get、put、remove等操作,LinkedHashMap除了要做HashMap做的事情,还做些调整Entry顺序链表的工作。LruCache中将LinkedHashMap的顺序设置为LRU顺序来实现LRU缓存,每次调用get(也就是从内存缓存中取图片),则将该对象移到链表的尾端。调用put插入新的对象也是存储在链表尾端,这样当内存缓存达到设定的最大值时,将链表头部的对象(近期最少用到的)移除。
LruCache源码
-
map:存放数据的集合
-
size:当前LruCahce的内存占用大小
-
maxSize:Lrucache的最大容量
-
putCount:put的次数
-
createCount:create的次数
-
evictionCount:回收的次数
-
hitCount:命中的次数
-
missCount:丢失的次数
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;
private int createCount;
private int evictionCount;
private int hitCount;
private int missCount;
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);
}
这里设置了maxSize,以及实例化了一个LinkedHashMap对象,这个LinkedHashMap对象是实现Lru算法的关键,Lru是最近最少使用算法的简称,意思呢就是查询出最近的时间使用次数最少的那个对象。
new LinkedHashMap<K, V>(0, 0.75f, true)这句代码表示,初始容量为零,0.75是加载因子,表示容量达到最大容量的75%的时候会把内存增加一半。最后这个参数至关重要。表示访问元素的排序方式。
true表示按照访问顺序排序,false表示按照插入的顺序排序。
我这里写了一个小程序专门研究这两个参数的不同之处。
当设置为true的时候:
public static final void main(String[] args) {
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(0, 0.75f, true);
map.put(0, 0);
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
map.put(4, 4);
map.put(5, 5);
map.put(6, 6);
map.get(1);
map.get(2);
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
输出结果是:
0:0
3:3
4:4
5:5
6:6
1:1
2:2
当设置为false的时候,输出顺序为:
0:0
1:1
2:2
3:3
4:4
5:5
6:6
有以上结果可以看出,这个设置为true的时候,如果对一个元素进行了操作(put、get),就会把那个元素放到集合的最后,设置为false的时候,无论怎么操作,集合元素的顺序都是按照插入的顺序来进行存储的。
到了这里我们可以知道,这个LinkedHashmap正是实现Lru算法的核心之处,当内容容量达到最大值的时候,只需要移除这个集合的前面的元素直到集合的容量足够存储数据的时候就可以了。
下面我们来看一看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++; //put的次数+1
size += safeSizeOf(key, value); //把当前容量增加,增加值为value的大小
previous = map.put(key, value); //previous为旧的值if (previous != null) {
size -= safeSizeOf(key, previous); //如果旧的值不为空,就把旧的值得大小减去
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
由上面的代码可以看出来,首先把size增加,然后判断是否以前已经有元素,如果有,就更新当前的size,并且调用entryRemoved方法,entryRemoved是一个空实现,如果我们使用LruCache的时候需要掌握元素移除的信息,可以重写这个方法。最后就会调用trimToSize,来调整集合中的内容。
trimToSize 的实现如下:
public void trimToSize(int maxSize) {
while(true) {
Object key;
Object value;
synchronized(this) {
if (this.size < 0 || this.map.isEmpty() && this.size != 0) {
throw new IllegalStateException(this.getClass().getName() + ".sizeOf() is reporting inconsistent results!");
}
if (this.size <= maxSize || this.map.isEmpty()) {
return;
}
Entry<K, V> toEvict = (Entry)this.map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
this.map.remove(key);
this.size -= this.safeSizeOf(key, value);
++this.evictionCount;
}
this.entryRemoved(true, key, value, (Object)null);
}
}
这个方法是一个无限循环,跳出循环的条件是,size < maxSize或者 map 为空。主要的功能是判断当前容量时候已经超出最大的容量,如果超出了maxSize的话,就会循环移除map中的第一个元素,直到达到跳出循环的条件。由上面的分析知道,map中的第一个元素就是最近最少使用的那个元素。
研究完了put方法之后,下面开始研究get方法。
@Nullable
public final V get(@NonNull K key) {
if (key == null) {
throw new NullPointerException("key == null");
} else {
Object mapValue;
synchronized(this) {
mapValue = this.map.get(key);
if (mapValue != null) {
++this.hitCount;
return mapValue;
}
++this.missCount;
}
V createdValue = this.create(key);
if (createdValue == null) {
return null;
} else {
synchronized(this) {
++this.createCount;
mapValue = this.map.put(key, createdValue);
if (mapValue != null) {
this.map.put(key, mapValue);
} else {
this.size += this.safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
this.entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
this.trimToSize(this.maxSize);
return createdValue;
}
}
}
}
这个方法就先通过key来获取value,如果能获取到就就直接返回,获取不到的话,就调用create()方法创建一个,事实上,如果我们不重写这个create方法的话是return null的,所以整个流程就是获取得到就直接返回,获取不到就返回null。至于后面那段代码呢?我看了几遍也没理解是适合什么场景的。反正就是重写的create方法之后就会执行后面的代码,不过我们通常使用的时候都是没有重写这个方法的。
最后说一下remove方法:
public final V remove(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;
}
remove方法没什么可以研究的了,就是使用remove方法移除一个元素。
LRU简单使用
public class ImageCache {
// 图片缓存
LruCache<String, Bitmap> mImageCache;
public ImageCache() {
initImageCache();
}
private void initImageCache() {
// 计算可使用的最大内存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 取4分之一的可用内存作为缓存
final int cacheSize = maxMemory / 4;
mImageCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
// 保存数据
public void put(String url, Bitmap bitmap) {
mImageCache.put(url, bitmap);
}
// 获取数据
public Bitmap get(String url) {
return mImageCache.get(url);
}
}