一.LruCache算法
LruCache算法就是Least Recently Used,也就是最近最少使用算法。
他的算法就是当缓存空间满了的时候,将最近最少使用的数据从缓存空间中删除以增加可用的缓存空间来缓存新内容。
LruCache算法内部其实是一个队列的形式在存储数据,先进来的数据放在队列的尾部,后进来的数据放在队列头部,如果要使用队列中的数据,那么使得之后将其又放在队列的头部,如果要存储数据并且发现数据已经满了,那么便将队列尾部的数据给剔除掉,从而达到我们使用缓存的目的。这里需要注意一点,队尾存储的数据就是我们最近最少使用的数据,也就是我们在内存满的时候需要剔除掉的数据。(注:这里说的队列概念不要跟下面源码里的链表的操作概念混淆在一起了)
二.LruCache部分源码
Least Recently Used,最近最少使用
下面只是部分源码
package android.util;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* LruCache通过强引用来缓存一定数量的值.
* 每当一个值被访问的时候,这个值就会移动到缓存队列的头部.
* 如果插入数据时发现缓存不够了,就会将队列中访问次数最少的数据删掉.
*
*/
public class LruCache<K, V> {
/**
* 真正放置缓存内容的map。
*/
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;
/**
* 构造方法,传入缓存的最大值maxSize。
*/
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
//初始化LinkedHashMap。
//第一个参数是初始容量
//第二个参数是填装因子,或叫加载因子
//第三个参数是排序模式,true表示在访问的时候进行排序,否则只在插入的时候才排序。
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
/**
* 重新设置最大缓存
*/
public void resize(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
synchronized (this) {
this.maxSize = maxSize;
}
trimToSize(maxSize);
}
/**
* 通过key获取缓存的数据,如果通过这个方法得到的需要的元素,
* 那么这个元素会被放在缓存队列的头部,
* 可以理解成最近常用的元素,不会在缓存空间不够的时候自动清理掉
*/
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
//这里用同步代码块,
synchronized (this) {
//从LinkedHashMap中获取数据。
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
/**
* 如果通过key从缓存集合中获取不到缓存数据,就尝试使用creat(key)方法创造一个新数据。
* create(key)默认返回的也是null,需要的时候可以重写这个方法。
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
//如果重写了create(key)方法,创建了新的数据,就讲新数据放入缓存中。
synchronized (this) {
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;
}
}
/**
* 往缓存中添加数据
*/
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++;
//safeSizeOf(key, value)。
// 这个方法返回的是1,也就是将缓存的个数加1.
// 当缓存的是图片的时候,这个size应该表示图片占用的内存的大小,
// 所以应该重写里面调用的sizeOf(key, value)方法
size += safeSizeOf(key, value);
//将创建的新元素添加进缓存队列,并添加成功后返回这个元素
previous = map.put(key, value);
if (previous != null) {
//如果返回的是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!");
}
//当已用的缓存小于最大缓存,完成任务,退出循环
if (size <= maxSize) {
break;
}
//否则就在缓存队列中先找到最近最少使用的元素,调用LinkedHashMap的eldest()方法返回最不经常使用的方法。
Map.Entry<K, V> 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 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;
}
/**
* 这个方法在前面很多地方都会被调用,默认是空方法,有需要的时候自己实现
* evicted如果是true,则表示这个元素是因为空间不够而被自动清理了,
* 所以可以在这个地方对这个被清理的元素进行再次缓存
*/
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
/**
* 一个空方法,也是在需要的时候重写实现
*/
protected V create(K key) {
return null;
}
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
/**
* 这个方法可以说是用来定义已用缓存的数量算法,默认是返回数量
*/
protected int sizeOf(K key, V value) {
return 1;
}
/**
* 清空所有缓存
*/
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}
.......
}
通过这个源码,可以发现,LruCache的算法实现主要是依靠LinkedHashMap来实现的。
三.为什么用LinkedHashMap
为什么要用LinkedHashMap来存缓存呢,这个跟算法有关,LinkedHashMap刚好能提供LRUCache需要的算法。
这个集合内部本来就有个排序功能,当第三个参数是true的时候,该LinkedHashMap是以访问顺序排序的。
如何实现
重写 removeEldestEntry方法即可
// 新建 LinkedHashMap
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(4,0.75f,true) {
{
put(10, 10);
put(9, 9);
put(20, 20);
put(1, 1);
}
@Override
// 覆写了删除策略的方法,我们设定当节点个数大于 3 时,就开始删除头节点
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > 3;
}
};
当我们调用 map.get(9) 方法时,元素 9 移动到队尾,调用 map.get(20) 方法时, 元素 20 被移动到队尾,这个体现了经常被访问的节点会被移动到队尾。
附上测试样例代码:
public static 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.get(1);
// map.get(2);
System.out.println("获取数据1:" + map.get(1));
System.out.println("获取数据2:" + map.get(2));
System.out.println("---------------");
System.out.println("1.遍历map :测试数据:");
System.out.println("---------------");
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
map.put(3, 3);
map.put(-1, -1);
System.out.println("---------------");
System.out.println("2.遍历map :测试数据:");
System.out.println("---------------");
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
返回结果:
获取数据1:1
获取数据2:2
---------------
1.遍历map :测试数据:
---------------
0:0
1:1
2:2
---------------
2.遍历map :测试数据:
---------------
0:0
1:1
2:2
3:3
-1:-1
3.1、get时为什么元素会放到队尾
public V get(Object key) {
Node<K,V> e;
// 调用 HashMap get 方法
if ((e = getNode(hash(key), key)) == null)
return null;
// 如果设置了 LRU 策略
if (accessOrder)
// 这个方法把当前 key 移动到队尾
afterNodeAccess(e);
return e.value;
}
从上述源码中,可以看到,如果 accessOrder 为true的话通过 afterNodeAccess 方法把当前访问节点移动到了队尾,其实不仅仅是 get 方法,执行 getOrDefault、compute、computeIfAbsent、computeIfPresent、merge 方法时,也会这么做,通过不断的把经常访问的节点移动到队尾,那么靠近队头的节点,自然就是很少被访问的元素了。
3.2、put时删除策略
在执行 put 方法时,发现队头元素被删除了,LinkedHashMap 本身是没有 put 方法实现的,调用的是 HashMap 的 put 方法,但 LinkedHashMap 实现了 put 方法中的调用 afterNodeInsertion 方法,这个方式实现了删除,我们看下源码:
// 删除很少被访问的元素,被 HashMap 的 put 方法所调用
void afterNodeInsertion(boolean evict) {
// 得到元素头节点
LinkedHashMap.Entry<K,V> first;
// removeEldestEntry 来控制删除策略,如果队列不为空,并且删除策略允许删除的情况下,删除头节点
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
// removeNode 删除头节点
removeNode(hash(key), key, null, false, true);
}
}
四.用LruCache来缓存Bitmap的初始化
public static void testLruCache(final Bitmap bitmap,String key) {
//supportV4的LruCache能兼容旧版本
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);//因为默认返回的byte,然后转换成KB
int lruMemory = maxMemory / 8;//缓存内存分配了可用内存的1/8。
LruCache<String, Bitmap> mMemoryCache = new LruCache<String, Bitmap>(lruMemory) {
@Override
protected int sizeOf(String key, Bitmap value) {
//重写sizeOf(),计算每张图片的缓存大小
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;//转换成KB,获取到的默认单位都是byte
}
};
//从缓存中获取对象
mMemoryCache.get(key);
//向缓存中添加对象,如果没有值才添加,有值同一个key就不用重复添加了
if(mMemoryCache.get(key) == null) {
mMemoryCache.put(key,bitmap);
}
mMemoryCache.remove(key);
}