Android知识总结
一、简介
LruCache(Least Recently Used)算法的核心思想就是最近最少使用算法
。他在算法的内部维护了一个LinkHashMap的链表
,LinkedHashMap 是由数组+双向链表
的数据结构来实现的,通过put数据的时候判断是否内存已经满了,如果满了,则将最近最少使用的数据给剔除掉,从而达到内存不会爆满的状态。
通过上面这张图,我们可以看到,LruCache算法内部其实是一个队列的形式在存储数据,先进来的数据放在队列的尾部,后进来的数据放在队列头部,如果要使用队列中的数据,那么使得之后将其又放在队列的头部
,如果要存储数据并且发现数据已经满了,那么便将队列尾部的数据给剔除掉
,从而达到我们使用缓存的目的。这里需要注意一点,队尾存储的数据就是我们最近最少使用的数据,也就是我们在内存满的时候需要剔除掉的数据。
二、代码分析
1、构造方法和参数
public class LruCache {
//定义一个LinkedHashMap,有序的 map
private final LinkedHashMap map;
private int size;//初始大小
private int maxSize;//最大容量
private int putCount;//插入个数
private int createCount;//创建个数
private int evictionCount;//回收个数
private int hitCount;//找到key的个数
private int missCount;//没找到key的个数
//构造函数,传递进来一个最大容量值
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
//赋值,初始化
this.maxSize = maxSize;
this.map = new LinkedHashMap(0, 0.75f, true);
}
//设置cache的大小
public void resize(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
synchronized (this) {
this.maxSize = maxSize;
}
trimToSize(maxSize);
}
}
2、 get 方法分析
//如果通过key查找到value存在于cache中就直接返回或者通过create方法创建一个然后返回
//如果这个值被返回了,那么它将移动到队列的头部
//如果一个值没有被缓存同时也不能被创建则返回null
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key); // 获取 Value
//找到对应值,命中+1,直接返回该值
if (mapValue != null) {
hitCount++;
return mapValue;
}
//否则未命中+1
missCount++;
}
//如果没找到key对应的值那么就尝试创建一个,也许花费较长的时间
//并且创建后返回的map也许和之前的不同,如果创建的值和map中有冲突的话
//那么我们就释放掉创建的值,保留map中的值。
V createdValue = create(key);
//通过观察后面的create()方法,可以看到直接return null;
//那么我们需要想一想为什么源码中是直接返回null呢?
//因为LruCache常常作为内存缓存而存在,所以当我们查找key找不到对应的value时
//这个时候我们应该从其他方面,比如文件缓存或者网络中请求数据
//而不是我们随便赋值创建一个值返回,所以这里返回null是合理的。
//如果自己真的有需要的话,自己需要重写create方法,手动创建一个值返回
if (createdValue == null) {
return null;
}
//走到这儿说明创建了一个不为null的值
synchronized (this) {
createCount++;//创建个数+1
//把创建的value插入到map对应的key中
//并且将原来键为key的对象保存到mapValue
mapValue = map.put(key, createdValue);
if (mapValue != null) {
//如果mapValue不为空,说明原来key对应的是有值的,则撤销上一步的put操作。
map.put(key, mapValue);
} else {
//加入新创建的对象之后需要重新计算size大小
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
//每次新加入对象都需要调用trimToSize方法看是否需要回收
trimToSize(maxSize);
return createdValue;
}
}
- 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;
}
get() 方法其实最关键就是 afterNodeAccess()
,现在重点分析:
// 这个方法的作用就是将刚访问过的元素放到集合的最后一位
void afterNodeAccess(Node < K, V > e) {
LinkedHashMap.Entry < K, V > last;
if (accessOrder && (last = tail) != e) {
// 将 e 转换成 LinkedHashMap.Entry
// b 就是这个节点之前的节点
// a 就是这个节点之后的节点
LinkedHashMap.Entry < K, V > p = (LinkedHashMap.Entry < K, V > ) e, b = p.before, a = p.after;
// 将这个节点之后的节点置为 null
p.after = null;
// b 为 null,则代表这个节点是第一个节点,将它后面的节点置为第一个节点
if (b == null) head = a;
// 如果不是,则将 a 上前移动一位
else b.after = a;
// 如果 a 不为 null,则将 a 节点的元素变为 b
if (a != null) a.before = b;
else last = b;
if (last == null) head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
3、put 方法分析
现在以注释的方式来解释该方法的原理。
//将key对应的value缓存起来,放在队列的头部
//返回key对应的之前的旧值
public final V put(K key, V value) {
// 如果 key 或者 value 为 null,则抛出异常
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
// 加入元素的数量,在 putCount() 用到
putCount++;//插入数量+1
// 回调用 sizeOf(K key, V value) 方法,这个方法用户自己实现,默认返回 1
size += safeSizeOf(key, value);
//得到key对应的前一个value,如果之前无值,返回null,如果有值,返回前一个值
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
// 该方法默认方法体为空
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
//返回之前key对应的旧值value
return previous;
}
//根据maxSize来调整内存cache的大小,如果maxSize传入-1,则清空缓存中的所有对象
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!");
}
//直到缓存大小 size 小于或等于最大缓存大小 maxSize,则停止循环
if (size <= maxSize) {
break;
}
// 取出 map 中第一个元素
Map.Entry toEvict = map.eldest();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
// 删除该元素
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;//回收个数+1
}
entryRemoved(true, key, value, null);
}
}
put() 方法其实重点就在于 trimToSize() 方法里面,这个方法的作用就是判断加入元素后是否超过最大缓存数,如果超过就清除掉最少使用的元素。
4、remove 方法
//从内存缓存中根据key值移除某个对象并返回该对象
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;
}
三、总结
- 为什么LruCache内部原理的实现需要用到LinkHashMap来存储数据呐?
因为LinkHashMap内部是一个数组加双向链表的形式来存储数据,他能够保证插入时候的数据和取出来的时候数据的顺序的一致性。也就是说,我们以什么样的顺序插入数据,就会以什么样的顺序取出数据。并且更重要的一点是,当我们通过get方法获取数据的时候,这个获取的数据会从队列中跑到队列头来,从而很好的满足我们LruCache的算法设计思想。
- LruCache 其实使用了 LinkedHashMap 维护了强引用对象?
总缓存的大小一般是可用内存的 1/8,当超过总缓存大小会删除最少使用的元素,也就是内部 LinkedHashMap 的头部元素。
当使用 get() 访问元素后,会将该元素移动到 LinkedHashMap 的尾部