一、基本概念
1.1 LruCache 的作用
LruCache
的基本思想是Least Recently Used
,即 最近最少使用,也就是当LruCache
内部缓存在内存中的对象大小之和到达设定的阈值后,会删除 访问时间距离当前最久 的对象,从而避免了OOM
的发生。
LruCache
特别适用于图片内存缓存这种有可能需要占用很多内存,但是只有最近使用的对象才有可能用到的场景。
1.2 LruCache 的使用
下面,我们用一个例子来演示一下LruCache
的使用,让大家有一个初步的认识。
public class LruCacheSamples {
private static final int MAX_SIZE = 50;
public static void startRun() {
LruCacheSample sample = new LruCacheSample();
Log.d("LruCacheSample", "Start Put Object1, size=" + sample.size());
sample.put("Object1", new Holder("Object1", 10));
Log.d("LruCacheSample", "Start Put Object2, size=" + sample.size());
sample.put("Object2", new Holder("Object2", 20));
Log.d("LruCacheSample", "Start Put Object3, size=" + sample.size());
sample.put("Object3", new Holder("Object3", 30));
Log.d("LruCacheSample", "Start Put Object4, size=" + sample.size());
sample.put("Object4", new Holder("Object4", 10));
}
static class LruCacheSample extends LruCache {
LruCacheSample() {
super(MAX_SIZE);
}
@Override
protected int sizeOf(String key, Holder value) {
return value.getSize();
}
@Override
protected void entryRemoved(boolean evicted, String key, Holder oldValue, Holder newValue) {
if (oldValue != null) {
Log.d("LruCacheSample", "remove=" + oldValue.getName());
}
if (newValue != null) {
Log.d("LruCacheSample", "add=" + newValue.getName());
}
}
}
static class Holder {
private String mName;
private int mSize;
Holder(String name, int size) {
mName = name;
mSize = size;
}
public String getName() {
return mName;
}
public int getSize() {
return mSize;
}
}
}
运行结果为:
在放入
Object3
之后,由于放入之前
LruCache
的大小为
30
,而
Object3
的大小为
30
,放入之后的大小为
60
,超过了最先设定的最大值
50
,因此会移除最先插入的
Object1
,减去该元素的大小
10
,最新的大小变为
50
。
二、源码解析
2.1 构造函数
首先看一下LruCache
的构造函数:
/**
* @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(0, 0.75f, true);
}
当我们创建一个LruCache
类时需要指定一个最大的阈值maxSize
,而我们的对象会缓存在LinkedHashMap
当中:
-
maxSize
等于LinkedHashMap
中每个元素的sizeOf(key, value)
之和,默认情况下每个对象的大小为1
,使用者可以通过重写sizeOf
指定对应元素的大小。 -
LinkedHashMap
是实现LRU
算法的核心,它会根据对象的使用情况维护一个双向链表,其内部的header.after
指向历史最悠久的元素,而header.before
指向最年轻的元素,这一“年龄”的依据可以是访问的顺序,也可以是写入的顺序。
2.2 put 流程
接下来看一下与put
相关的方法:
/**
* Caches {@code value} for {@code key}. The value is moved to the head of
* the queue.
*
* @return the previous value mapped by {@code key}.
*/
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++;
//获得该对象的大小,由 LruCache 的使用者来决定,要求返回值大于等于 0,否则抛出异常。
size += safeSizeOf(key, value);
//调用的是 HashMap 的 put 方法,previous 是之前该 key 值存放的对象。
previous = map.put(key, value);
//如果已经存在,由于它现在被替换成了新的 value,所以需要减去这个大小。
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
//通知使用者该对象被移除了。
if (previous != null) {
entryRemoved(false, key, previous, value);
}
//由于放入了新的对象,因此需要确保目前总的容量没有超过设定的阈值。
trimToSize(maxSize);
return previous;
}
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
/**
* 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.
*
* An entry's size must not change while it is in the cache.
*/
protected int sizeOf(K key, V value) {
//默认情况下,每个对象的权重值为 1。
return 1;
}
/**
* Remove the eldest entries until the total of remaining entries is at or
* below the requested size.
*
* @param maxSize the maximum size of the cache before returning. May be -1
* to evict even 0-sized elements.
*/
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!");
}
//这是一个 while 循环,因此将一直删除最悠久的结点,直到小于阈值。
if (size <= maxSize) {
break;
}
//获得历史最悠久的结点。
Map.Entry toEvict = map.eldest();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
//从 map 中将它移除。
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
//通知使用者该对象被移除了。
entryRemoved(true, key, value, null);
}
}
关于代码的解释都在注释中了,其核心的思想就是在每放入一个元素之后,通过sizeOf
来获得这个元素的权重值,如果发现所有元素的权重值之和大于size
,那么就通过trimToSize
移除历史最悠久的元素,并通过entryRemoved
回调给LruCache
的实现者。
2.3 get 流程
/**
* Returns the value for {@code key} if it exists in the cache or can be
* created by {@code #create}. If a value was returned, it is moved to the
* head of the queue. This returns null if a value is not cached and cannot
* be created.
*/
public final V get(K key) {
//与 HashMap 不同,LruCache 不允许 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++;
}
//如果在 map 中没有找到,get 方法不会直接返回 null,而是先回调 create 方法,让使用者有一个创建的机会。
V createdValue = create(key);
//如果使用者没有重写 create 方法,那么会返回 null。
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
//由于 create 的过程没有加入同步块,因此有可能在创建的过程中,使用者通过 put 方法在 map 相同的位置放入了一个对象,这个对象是 mapValue。
mapValue = map.put(key, createdValue);
//如果存在上面的情况,那么会抛弃掉 create 方法创建对象,重新放入已经存在于 map 中的对象。
if (mapValue != null) {
map.put(key, mapValue);
} else {
//增加总的权重大小。
size += safeSizeOf(key, createdValue);
}
}
//如果存在冲突的情况,那么要通知使用者这一变化,但是有大小并没有改变,所以不需要重新计算大小。
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
//由于大小改变了,因此需要重新计算大小。
trimToSize(maxSize);
return createdValue;
}
}
这里需要特别说明一下LruCache
与HashMap
的get
方法的区别:如果LinkedHashMap
中不存在Key
对应的Value
,get
方法并像HashMap
一样直接返回,而是先 通过create
方法尝试让使用者重新创建一个对象,如果创建成功,那么将会把这个对象放入到集合当中,并返回这个新创建的对象。
上面这种是单线程的情况,如果在多线程的情况下,由于create
方法没有加入synchronized
关键字,因此有可能 一个线程在create
方法创建对象的过程中,另一个线程又通过put
方法在Key
对应的相同位置放入一个对象,在这种情况下,将会抛弃掉由create
创建的对象,维持原有的状态。
2.4 LinkedHashMap
通过get/set
方法,我们可以知道LruCache
是通过trimToSize
来保证它所维护的对象的权重之和不超过maxSize
,最后我们再来分析一下LinkedHashMap
,看下它是如何保证每次大小超过maxSize
时,移除的都是历史最悠久的元素的。
LinkedHashMap
继承于HashMap
,它通过重写相关的方法在HashMap
的基础上实现了双向链表的特性。
2.4.1 Entry 元素
LinkedHashMap
重新定义了HashMap
数组中的HashMapEntry
,它的实现为LinkedHashMapEntry
,除了原有的next
、key
、value
和hash
值以外,它还额外地保存了after
和before
两个指针,用来实现根据写入顺序或者读取顺序来排列的双向链表。
private static class LinkedHashMapEntry extends HashMapEntry {
LinkedHashMapEntry before, after;
LinkedHashMapEntry(int hash, K key, V value, HashMapEntry next) {
super(hash, key, value, next);
}
//删除链表结点。
private void remove() {
before.after = after;
after.before = before;
}
//在 existingEntry 之前插入该结点。
private void addBefore(LinkedHashMapEntry existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
//如果是按访问顺序排列,那么将该结点插入到整个链表的头部。
void recordAccess(HashMap m) {
LinkedHashMap lm = (LinkedHashMap)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
//从链表中移除该结点。
void recordRemoval(HashMap m) {
remove();
}
}
2.4.2 初始化
LinkedHashMap
重写了init()
方法,该方法会在其父类HashMap
的构造函数中被调用,在init()
方法中,会初始化一个空的LinkedHashMapEntry
结点header
,它的before
指向最年轻的元素,而after
指向历史最悠久的元素。
void init() {
header = new LinkedHashMapEntry<>(-1, null, null, null);
header.before = header.after = header;
}
在LinkedHashMap
的构造函数中,可以传入accessOrder
,如果accessOrder
为true
,那么“历史最悠久”的元素表示的是访问时间距离当前最久的元素,即按照访问顺序排列;如果为false
,那么表示最先插入的元素,即按照插入顺序排列,默认的值为false
。
2.4.3 元素写入
对于元素的写入,LinkedHashMap
并没有重写put
方法,而是重写了addEntry/createEntry
方法,在创建结点的同时,更新它所维护的双向链表。
void addEntry(int hash, K key, V value, int bucketIndex) {
LinkedHashMapEntry eldest = header.after;
if (eldest != header) {
boolean removeEldest;
size++;
try {
removeEldest = removeEldestEntry(eldest);
} finally {
size--;
}
if (removeEldest) {
removeEntryForKey(eldest.key);
}
}
super.addEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry old = table[bucketIndex];
LinkedHashMapEntry e = new LinkedHashMapEntry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);
size++;
}
2.4.4 元素读取
对于元素的读取,LinkedHashMap
重写了get
方法,它首先调用HashMap
的getEntry
方法找到结点,如果判断是需要根据访问的顺序来排列双向列表,那么就需要对链表进行更新,即调用我们在2.4.1
中看到的recordAccess
方法。
public V get(Object key) {
LinkedHashMapEntry e = (LinkedHashMapEntry)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
三、参考文献
深入 Java 集合学习系列:LinkedHashMap 的实现原理