背景
LruCache这个很常见,做过android的开发没见过也都听说过这个,一般应用常见就是做缓存的时候用到,说一下我与LruCache的故事吧,好多年面试的时候让我实现一下Lru算法,我当时用HashMap搞的,搞的好复杂,其实你看Android的中LruCahe类很简单,就三百多行代码
什么是Lru算法呢?
LRU是Least Recently Used的缩写,即最近最少使用,常用于页面置换算法,是为虚拟页式存储管理服务的。
android中是如何实现的呢?
*/
public class LruCache {
private final LinkedHashMap map;
//缓存的数据大小
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(0, 0.75f, true);
}
}
别看上面的代码很简单,关键的就是LinkedHashMap,这个是实现LRU算法的关键,所有的功能都是基于LinkedHashMap实现的
this.map = new LinkedHashMap(0, 0.75f, true);
这个代码很关键,LinkedHashMap最后一个参数true很关键,这个true决定了LinkedHashMap遍历的访问顺序,
true表示迭代的时候安装访问顺序迭代,false表示访问的时候安装插入书序迭代,为了解释这个问题我们先上一段代码
/**
* @author nate
* @since 2018/8/6.
*/
public class Test {
public static void main(String args[]) {
LinkedHashMap linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("1", "1");
linkedHashMap.put("2", "2");
linkedHashMap.put("3", "3");
linkedHashMap.put("4", "4");
linkedHashMap.get("1");
linkedHashMap.get("2");
linkedHashMap.get("2");
for (String s : linkedHashMap.keySet()) {
System.out.println(s);
}
}
}
输出的结果是:
1
2
3
4
即便我们代码中访问两次key为2的数据,对输出结果没有任何影响,这个输出的结果就是安装插入顺序迭代的
调整一下代码,我们再创建LinkHashMap的时候制定按照访问顺序排序访问
public static void main(String args[]) {
LinkedHashMap linkedHashMap = new LinkedHashMap(0,0.75f,true);
linkedHashMap.put("1", "1");
linkedHashMap.put("2", "2");
linkedHashMap.put("3", "3");
linkedHashMap.put("4", "4");
linkedHashMap.get("1");
linkedHashMap.get("2");
linkedHashMap.get("2");
for (String s : linkedHashMap.keySet()) {
System.out.println(s);
}
}
输出结果:
3
4
1
2
这个地方比较关键的是我们访问了两次key为2的数据,意思key为2的数据是最后的迭代的,没有访问过的数据是先迭代的,这就是LRU的关键
我们分析一下get代码
public final V get(K key) {
//对key进行非空判断
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//首先获取map中的数据
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.
*/
//试图去创建数据,默认的实现是返回null的
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
//创建数量+1
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;
}
}
总结一下:
- 首先在缓存中查找数据,如果有数据就返回,hitCount++
- 如果找不到,就看是否实现了create 方法,默认是空实现的
- 如果实现了create 方法,先把创建的数据放到缓存中,如果缓存已经有了数据,则执行撤销操作,并且要回调entryRemoved 执行释放操作
- 计算一下总体的数据大小
如何移除无用的数据呢?
移除数据的操作主要是在put方法中回调的,如果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) {
//put数量++
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;
}
计算超过最大值移除逻辑
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!");
}
//如果缓存的数据<=maxSize return
if (size <= maxSize) {
break;
}
//移除无用的代码
Map.Entry 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);
}
}
获取相关记录的数据
public synchronized final int maxSize() {
return maxSize;
}
/**
* Returns the number of times {@link #get} returned a value that was
* already present in the cache.
*/
public synchronized final int hitCount() {
return hitCount;
}
/**
* Returns the number of times {@link #get} returned null or required a new
* value to be created.
*/
public synchronized final int missCount() {
return missCount;
}
/**
* Returns the number of times {@link #create(Object)} returned a value.
*/
public synchronized final int createCount() {
return createCount;
}
/**
* Returns the number of times {@link #put} was called.
*/
public synchronized final int putCount() {
return putCount;
}
/**
* Returns the number of values that have been evicted.
*/
public synchronized final int evictionCount() {
return evictionCount;
}
/**
统计所有的数据
@Override public synchronized final String toString() {
int accesses = hitCount + missCount;
int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
maxSize, hitCount, missCount, hitPercent);
}
总结
- LruCache 本身的源代码也是比较简单的,主要逻辑的都LinkedHashMap 实现的,搞懂LinkedHashMap也就搞懂了LruCache