参考文章:
Android使用LruCache缓存 - 火神漫步 - 博客园
一、简介
安卓系统对内存的消耗的条件是相当苛刻的,肆意使用内存会导致系统OOM并杀死程序,所以对于每个APP在使用内存时就要谨慎再谨慎。
LRU是近期最少使用的算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LrhCache和DisLruCache,分别用于实现内存缓存和硬盘缓存,其核心思想都是LRU缓存算法。
从android3.1开始,LruCache已经作为android源码的一部分维护在android系统中,为了兼容以前的版本,android的support-v4包也提供了LruCache的维护,如果App需要兼容到android3.1之前的版本就需要使用support-v4包中的LruCache,如果不需要兼容到android3.1则直接使用android源码中的LruCache即可,但DiskLruCache并不是android源码的一部分。
二、手写LRU
在使用LruCache库之前,不妨先思考如何手动实现LRU算法,这样可以更好的理解LruCache的实现。而实现LRU,就不得不提到LinkedHashMap。
2.1 LinkedHashMap
LinkedHashMap继承自HashMap,它的多种操作都是建立在HashMap操作的基础上的。同HashMap不同的是,LinkedHashMap维护了一个Entry的双向链表,保证了插入的Entry中的顺序。这也是Linked的含义。结构图如下:
插入顺序为key1,key2,key3,key4,那么就会维护一个红线所示的双向链表。
可以说,LinkedHashMap=HashMap+双向链表
利用LinkedHashMap的特点,既可以很方便的实现LRU算法,又可以高效的访问元素,即每次命中元素,都将该元素置于链表尾部(先删除,再添加),这样就能保证头部元素是最久未使用的元素,一旦缓存空间占满,则可替换掉头部元素,而LinkedHashMap也为我们提供了标志位accessOrder
和removeEldestEntry(eldest)
方法来实现LRU算法。
- 如果accessOrder为true的话,则会把访问过的元素放在链表后面,放置顺序是访问的顺序
removeEldestEntry(eldest)
供用户覆写,如果返回true,则删除最久未使用的元素
2.2 get操作源码
/**
* 调用hashmap的getNode方法获取到值之后,维护链表
* @param key
* @return
*/
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;
}
复制代码
afterNodeAccess会将新插入的节点放到尾部,同时这个方法受到accessOrder的控制。
//在节点被访问后根据accessOrder判断是否需要调整链表顺序
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
//如果accessOrder为false,什么都不做
if (**accessOrder** && (last = tail) != e) {
//p指向待删除元素,b执行前驱,a执行后驱
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//这里执行双向链表删除操作
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
//这里执行将p放到尾部
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
//保证并发读安全。
++modCount;
}
}
复制代码
2.3 addEntry源码
void addEntry(int hash, K key, V value, int bucketIndex) {
//创建新的Entry,并插入到LinkedHashMap中
createEntry(hash, key, value, bucketIndex); // 重写了HashMap中的createEntry方法
//双向链表的第一个有效节点(header后的那个节点)为最近最少使用的节点,这是用来支持LRU算法的
Entry<K,V> eldest = header.after;
//如果有必要,则删除掉该近期最少使用的节点,
//这要看对removeEldestEntry的覆写,由于默认为false,因此默认是不做任何处理的。
if (**removeEldestEntry(eldest)** ) {
removeEntryForKey(eldest.key);
} else {
//扩容到原来的2倍
if (size >= threshold)
resize(2 * table.length);
}
}
复制代码
这里通过removeEldestEntry(eldest)控制是否删除近期最少使用的节点。
2.4 使用LinkedHashMap实现LRU
class LRUCache extends LinkedHashMap {
private int capacity;
public LRUCache(int capacity) {
//accessOrder为true
super(capacity, 0.75F, true);
this.capacity = capacity;
}
public int get(int key) {
return (int)super.getOrDefault(key, -1);
}
public void put(int key, int value) {
super.put(key, value);
}
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > capacity;
}
}
复制代码
三、LruCache库
3.1 介绍
LruCache库内部也是使用LinkedHashMap对数据进行管理:
public class LruCache<K, V> {
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; // put次数
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;
//accessOrder为true,即按访问排序
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
}
复制代码
- 必须覆写的方法:
- protected int sizeOf(K key, V value):计算每个对象占用的空间大小,供LruCache控制缓存大小
在重写了 sizeOf 方法的情况下,maxSize 代表的就是我们每个元素大小之后累加的允许的最大值。
- 可以覆写的方法
- protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) :删除元素时的额外操作,比如释放资源
- protected V create(K key):创建元素
3.2 关键代码分析
3.2.1 put() 添加缓存
public final V put(K key, V value) {
// 键值对不可为空
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
// 旧值
V previous;
// 同步代码块,使用 this 也就是说同时只能由一个线程操作这个对象
synchronized (this) {
putCount++;
// 先通过safeSizeOf方法计算当前传入的 value 的大小,累加的 size
size += safeSizeOf(key, value);
// 把键值对插入到 LinkedHashMap 中,如果有返回值,说明存在相同的 key,取出旧值给 previous
previous = map.put(key, value);
// 如果存在旧值,则从当前大小中删除旧值占用的大小.
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
// 如果 存在旧值,相当于把旧值移除了,这里调用 entryRemoved 方法.
// entryRemoved 默认是空实现,如果用户有需求,可以自己实现,完成一些资源的释放工作.
if (previous != null) {
entryRemoved(false, key, previous, value);
}
// 这个是最关键的方法,用来计算当前大小是否符合要求.
trimToSize(maxSize);
// 返回旧值
return previous;
}
复制代码
方法的最后调用了trimToSize(maxSize) 方法,这个方法是个核心方法,主要计算当前大小是否超过了设置的最大值,超过了则会将最近最少使用的元素移除。
3.2.2 trimToSize() 控制缓存的容量
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 || map.isEmpty()) {
break;
}
// 使用 map.entrySet() 代表从 LinkedHashMap 的头结点开始遍历,在
// 从头开始遍历,那只取一次,toEvict 就是头节点的元素
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
// 要删除元素的 key
key = toEvict.getKey();
// 要删除元素的 value
value = toEvict.getValue();
// 使用 LinkedHashMap 的 remove 方法删除指定元素
map.remove(key);
// 重新计算当前 size 的大小
size -= safeSizeOf(key, value);
// 移除次数+1
evictionCount++;
}
// 调用用户自定义的 entryRemoved() 如果用户定义了的话
entryRemoved(true, key, value, null);
}
}
复制代码
3.3.3 remove() 删除缓存
public final V remove(K key) {
// 不允许 null 值
if (key == null) {
throw new NullPointerException("key == null");
}
// 删除的元素
V previous;
// 同步代码块保证线程安全
synchronized (this) {
// 删除元素,并把值赋给 previous
previous = map.remove(key);
//如果之前有 key 对应的值,将其减去
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
// 如果用户重写了entryRemoved 并且 之前有与 key 对应的值,执行entryRemoved。
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
复制代码
3.3.4 get() 获取缓存
public final V get(K key) {
// 不允许 null key
if (key == null) {
throw new NullPointerException("key == null");
}
// value 的值
V mapValue;
// 同步代码块保证当前实例的线程安全
synchronized (this) {
// 通过 LinkedHashMap 的 get 方法去寻找
mapValue = map.get(key);
// 找到只,直接返回,命中值 +1
if (mapValue != null) {
hitCount++;
return mapValue;
}
// 没找到,未命中次数+1
missCount++;
}
// 这个地方意识,没有通过 get 方法找到,但是你想要有返回值,那么久可以重写 create 方法自己创建一个 返回值、。
V createdValue = create(key);
// 创建的值为 null ,直接返回 null
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
//将createdValue加入到map中,并且将原来键为key的对象保存到mapValue
mapValue = map.put(key, createdValue);
// 原来位置不为空,
if (mapValue != null) {
// There was a conflict so undo that last put
// 撤销上一步的操作,依旧把原来的值放到缓存。,替换掉新创建的值
map.put(key, mapValue);
} else {
// 原来key 对应的没值,计算当前缓存大小。
size += safeSizeOf(key, createdValue);
}
}
// 相当于一个替换操作,先用 createdValue 替换原来的值,然后这里移除掉 createdValue 。返回原来 key 对应的值。
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
// 调用trimToSize方法看是否需要回收旧数据
trimToSize(maxSize);
return createdValue;
}
}
复制代码
3.3.5 evictAll() 清除全部缓存数据
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}
复制代码
当执行到map.isEmpty()时,循环才会停止
if (size <= maxSize || map.isEmpty()) {
break;
}
复制代码
3.3 总结
- LruCache,内部使用Map保存内存级别的缓存(可手动操作映射到磁盘缓存)
- LruCache使用泛型可以设配各种类型
- LruCache使用了Lru算法保存数据
- LruCache只用使用put和get方法压入数据和取出数据