一,缓存机制的重要性
- 服务器的压力大大减小;
- 客户端的响应速度大大变快(用户体验好);
- 客户端的数据加载出错情况大大较少,大大提高了应有的稳定性(用户体验好);
- 一定程度上可以支持离线浏览(或者说为离线浏览提供了技术支持)。
内存缓存——LruCache源码分析
LRU,全称Least Rencetly Used,即最近最少使用,是一种非常常用的置换算法,也即淘汰最长时间未使用的对象。LRU在操作系统中的页面置换算法中广泛使用,我们的内存或缓存空间是有限的,当新加入一个对象时,造成我们的缓存空间不足了,此时就需要根据某种算法对缓存中原有数据进行淘汰货删除,而LRU选择的是将最长时间未使用的对象进行淘汰
LruCache实现原理
根据LRU算法的思想,要实现LRU最核心的是要有一种数据结构能够基于访问顺序来保存缓存中的对象,这样我们就能够很方便的知道哪个对象是最近访问的,哪个对象是最长时间未访问的。LruCache选择的是LinkedHashMap这个数据结构,LinkedHashMap是一个双向循环链表,在构造LinkedHashMap时,通过一个boolean值来指定LinkedHashMap中保存数据的方式,LinkedHashMap的一个构造方法如下:
/*
* 初始化LinkedHashMap
* 第一个参数:initialCapacity,初始大小
* 第二个参数:loadFactor,负载因子=0.75f
* 第三个参数:accessOrder=true,基于访问顺序;accessOrder=false,基于插入顺序
*/
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
init();
this.accessOrder = accessOrder;
}
显然,在LruCache中选择的是accessOrder = true;此时,当accessOrder 设置为 true时,每当我们更新(即调用put方法)或访问(即调用get方法)map中的结点时,LinkedHashMap内部都会将这个结点移动到链表的尾部,因此,在链表的尾部是最近刚刚使用的结点,在链表的头部是是最近最少使用的结点,当我们的缓存空间不足时,就应该持续把链表头部结点移除掉,直到有剩余空间放置新结点。
可以看到,LinkedHashMap完成了LruCache中的核心功能,那LruCache中剩下要做的就是定义缓存空间总容量,当前保存数据已使用的容量,对外提供put、get方法。
LruCache源码分析
1.关键字段
private final LinkedHashMap<K, V> map;
/** Size of this cache in units. Not necessarily the number of elements. */
// 当前缓存数据所占的大小
private int size;
//缓存空间总容量
private int maxSize;
要注意的是size字段,因为map中可以存放各种类型的数据,这些数据的大小测量方式也是不一样的,比如Bitmap类型的数据和String类型的数据计算他们的大小方式肯定不同,因此,LruCache中在计算放入数据大小的方法sizeOf中,只是简单的返回了1,
/**
* Returns the size of the entry for {@code key} and {@code value} in
* user-defined units. The default implementation returns 1 so that size
* is the number of entries and max size is the maximum number of entries.
*
* <p>An entry's size must not change while it is in the cache.
*/
protected int sizeOf(K key, V value) {
return 1;
}
需要我们重写这个方法,自己去定义数据的测量方式。因此,我们在使用LruCache的时候,经常会看到这种方式:
private static final int CACHE_SIZE = 4 * 1024 * 1024;//4Mib
LruCache bitmapCache = new LruCache(CACHE_SIZE){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();//自定义Bitmap数据大小的计算方式
}
};
2.构造方法
/**
* @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核心数据结构,在LinkedHashMap中的第三个参数指定为true,也就设置了accessOrder=true,表示这个LinkedHashMap将是基于数据的访问顺序进行排序。
3.put方法缓存数据
/**
* 给对应key缓存value,并且将该value移动到链表的尾部。
*/
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++;
// 通过键值对,计算出要保存对象value的大小,并更新当前缓存大小
size += safeSizeOf(key, value);
/*
* 如果 之前存在key,用新的value覆盖原来的数据, 并返回 之前key 的value
* 记录在 previous
*/
previous = map.put(key, value);
// 如果之前存在key,并且之前的value不为null
if (previous != null) {
// 计算出 之前value的大小,因为前面size已经加上了新的value数据的大小,此时,需要再次更新size,减去原来value的大小
size -= safeSizeOf(key, previous);
}
}
// 如果之前存在key,并且之前的value不为null
if (previous != null) {
/*
* previous值被剔除了,此次添加的 value 已经作为key的 新值
* 告诉 自定义 的 entryRemoved 方法
*/
entryRemoved(false, key, previous, value);
}
//裁剪缓存容量(在当前缓存数据大小超过了总容量maxSize时,才会真正去执行LRU)
trimToSize(maxSize);
return previous;
}
可以看到,put()方法主要有以下几步:
1)key和value判空,说明LruCache中不允许key和value为null;
2)通过safeSizeOf()获取要加入对象数据的大小,并更新当前缓存数据的大小;
3)将新的对象数据放入到缓存中,即调用LinkedHashMap的put方法,如果原来存在该key时,直接替换掉原来的value值,并返回之前的value值,得到之前value的大小,更新当前缓存数据的size大小;如果原来不存在该key,则直接加入缓存即可;
4)清理缓存空间,如下;
5.trimToSize()清理缓存空间
当我们加入一个数据时(put),为了保证当前数据的缓存所占大小没有超过我们指定的总大小,通过调用trimToSize()来对缓存空间进行管理控制
public void trimToSize(int maxSize) {
/*
* 循环进行LRU,直到当前所占容量大小没有超过指定的总容量大小
*/
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 || map.isEmpty()) {
break;
}
/**
* 执行到这,表示当前缓存数据已超过了总容量,需要执行LRU,即将最近最少使用的数据清除掉,直到数据所占缓存空间没有超标;
* 根据前面的原理分析,知道,在链表中,链表的头结点是最近最少使用的数据,因此,最先清除掉链表前面的结点
*/
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
// 移除掉后,更新当前数据缓存的大小
size -= safeSizeOf(key, value);
// 更新移除的结点数量
evictionCount++;
}
/*
* 通知某个结点被移除,类似于回调
*/
entryRemoved(true, key, value, null);
}
}
6.get获取缓存数据
/**
* 根据key查询缓存,如果该key对应的value存在于缓存,直接返回value;
* 访问到这个结点时,LinkHashMap会将它移动到双向循环链表的的尾部。
* 如果如果没有缓存的值,则返回null。(如果开发者重写了create()的话,返回创建的value)
*/
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
// LinkHashMap 如果设置按照访问顺序的话,这里每次get都会重整数据顺序
mapValue = map.get(key);
// 计算 命中次数
if (mapValue != null) {
hitCount++;
return mapValue;
}
// 计算 丢失次数
missCount++;
}
/*
* 官方解释:
* 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。如果在create()执行的时
* 候,用这个key执行了put方法,那么此时就发生了冲突,我们在Map中删除这个创建的值,释放被创建的值,保留put进去的值。
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
/***************************
* 不覆写create方法走不到下面 *
***************************/
/*
* 正常情况走不到这里
* 走到这里的话 说明 实现了自定义的 create(K key) 逻辑
* 因为默认的 create(K key) 逻辑为null
*/
synchronized (this) {
// 记录 create 的次数
createCount++;
// 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值
mapValue = map.put(key, createdValue);
// 如果之前存在相同key的value,即有冲突。
if (mapValue != null) {
/*
* 有冲突
* 所以 撤销 刚才的 操作
* 将 之前相同key 的值 重新放回去
*/
map.put(key, mapValue);
} else {
// 拿到键值对,计算出在容量中的相对长度,然后加上
size += safeSizeOf(key, createdValue);
}
}
// 如果上面 判断出了 将要放入的值发生冲突
if (mapValue != null) {
/*
* 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了
* 告诉 自定义 的 entryRemoved 方法
*/
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
// 上面 进行了 size += 操作 所以这里要重整长度
trimToSize(maxSize);
return createdValue;
}
}