Step1、示例
考虑用图片来模拟太麻烦,就弄个简单的类吧:
public class LruCacheTestActivity extends DefaultActivity {
static final String tag = "lru";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getButton().setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
test();
}
});
}
class Node {
int v1;
int v2;
double d1;
double d2;
public Node(int v1, int v2, double d1, double d2) {
this.v1 = v1;
this.v2 = v2;
this.d1 = d1;
this.d2 = d2;
}
int getSize() {
return 24;
}
@Override
public String toString() {
return "Node{" +
"v1=" + v1 +
", v2=" + v2 +
", d1=" + d1 +
", d2=" + d2 +
'}';
}
}
private void test() {
int cacheSize = 100;
LruCache<Integer, Node> lruCache = new LruCache<Integer, Node>(cacheSize) {
@Override
protected int sizeOf(@NonNull Integer key, @NonNull Node value) {
return value.getSize();
}
@Override
protected void entryRemoved(boolean evicted, @NonNull Integer key, @NonNull Node oldValue, @Nullable Node newValue) {
Log.d(tag, "entryRemoved: " + evicted + ", " + key + ", " + oldValue + ", " + newValue);
}
};
for (int i = 0; i < 4; i++) {
lruCache.put(i, new Node(i, i, i, i));
}
lruCache.put(4, new Node(4, 4, 4, 4));
lruCache.put(5, new Node(5, 5, 5, 5));
}
}
结果输出:
2020-04-10 13:28:15.345 24018-24018/com.amurocloud.testapp D/lru: entryRemoved: true, 0, Node{v1=0, v2=0, d1=0.0, d2=0.0}, null
2020-04-10 13:28:15.345 24018-24018/com.amurocloud.testapp D/lru: entryRemoved: true, 1, Node{v1=1, v2=1, d1=1.0, d2=1.0}, null
Node里面随便塞了些数据,数据大小就是4 * 2 + 8 * 2 = 24(不考虑引用等其他内存占用,单纯为了测试),如果什么都不干的话,从输出可以看出最早进入缓存的2个元素被删除了,剩余4个的大小正好是24 * 4 = 96 < 100,符合预期。
但是我们修改下test方法,改成:
for (int i = 0; i < 4; i++) {
lruCache.put(i, new Node(i, i, i, i));
}
lruCache.get(0);
lruCache.put(4, new Node(4, 4, 4, 4));
lruCache.put(5, new Node(5, 5, 5, 5));
结果输出:
2020-04-10 13:29:16.695 24142-24142/com.amurocloud.testapp D/lru: entryRemoved: true, 1, Node{v1=1, v2=1, d1=1.0, d2=1.0}, null
2020-04-10 13:29:16.695 24142-24142/com.amurocloud.testapp D/lru: entryRemoved: true, 2, Node{v1=2, v2=2, d1=2.0, d2=2.0}, null
可见0元素没有被删除,而其后的两个节点被删除了,cache内部还是4个元素。
其实这个实现的过程就是LruCache的缓存策略,即Lru–>(Least recent used)最少最近使用算法。
那LruCache具体如何实现这个功能的呢,我们开始看源码。
Step2、构造函数
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
//缓存实体就是个LinkedHashMap,注意最后一个参数传的true,看下面LHM的代码我们可以知道,这里创建的是一个按照最近访问顺序来排列的LHM
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
//LinkedHashMap核心调用的是HashMap的构造函数
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
//LinkedHashMap覆写了HashMap的init方法,创建了一个头节点来记录顺序
@Override
void init() {
header = new Entry<>(-1, null, null, null);
header.before = header.after = header;
}
那么LinkedHashMap和普通HashMap的区别是什么呢,看下面一张图:
对比HashMap的话看得更明白:
就是比HashMap多了两个指针before,after,来标识元素的顺序关系,另外还多了个头节点来标识队首,很经典的设计啊~
Step3、增删改查
一、增加或修改元素
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++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
//如果已经存在的元素,把刚才加上去size还原
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;
}
//这个就是我们在示例里覆写的方法,告诉LruCache如何获取元素的大小
protected int sizeOf(K key, V value) {
return 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!");
}
//满足设置的空间条件时退出
if (size <= maxSize || map.isEmpty()) {
break;
}
// 得到的就是双向链表表头header的下一个Entry
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);
}
}
整体来说添加元素的代码比较简单,就是每次添加完元素后检查空间,如果空间超出阈值就把队尾(最久没有被访问)的数据删除。
深入看下LinkedHashMap的put方法:
//LHM调用的也是HashMap的put方法,但是覆写了addEntry和createEntry方法
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//节点已经存在的的情况下,更新value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
//LHM覆写
e.recordAccess(this);
return oldValue;
}
}
//增加新节点
modCount++;
//LHM覆写
addEntry(hash, key, value, i);
return null;
}
//LHM覆写了HashMap的recordAccess方法,可以看到,如果设置了按访问顺序排序,当修改节点value时,会把这个元素从原来位置删除后插入到队尾。
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
void addEntry(int hash, K key, V value, int bucketIndex) {
super.addEntry(hash, key, value, bucketIndex);
// Remove eldest entry if instructed
//这个方法默认返回false,所以外部不覆写的话,不用管
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
}
}
//HashMap的addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
//扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
//LHM覆写的createEntry方法
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);
size++;
}
//Entry的addBefore方法,这里existingEntry传的是header,也就是把这个元素放入到了队尾
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry; // e.after = header
before = existingEntry.before; //e.before = header.before
before.after = this; // header.before.after = e
after.before = this; //header.before = e
}
总结就是新元素除了会正常放入到hashmap外,还会插入到队列的队尾,如果设置了按照访问顺序排序,更新Value时也会执行同样的操作。
二、访问元素
public final V get(@NonNull K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
//LHM中存在key,拿出元素并返回,同时根据前面描述的LHM的机制,此时这个节点会被更新到队尾
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
/*
* Attempt to create a value. This may take a long time, and the map
* may be different when create() returns. If a conflicting value was
* added to the map while create() was working, we leave that value in
* the map and release the created value.
*/
//这里设计了一个在key对应节点不存在时,允许用户自己创建一个新节点的回调,正常返回null,不影响主流程
V createdValue = create(key);
if (createdValue == null) {
return null;
}
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;
}
}
//LHM覆写了HashMap的get方法,核心就是上面分析过的recordAccess方法,在设置了按照访问顺序排序后,一旦被get就会被放到队尾中。
public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
三、删除元素
public final V remove(@NonNull K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V previous;
synchronized (this) {
//实际调用的是HashMap的remove方法
previous = map.remove(key);
//删除元素存在时,更新容量
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
//HashMap的remove方法
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
//LHM覆写了这个方法做队列的调整
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
//LHM覆写的方法
void recordRemoval(HashMap<K,V> m) {
remove();
}
//remove方法在Entry里,就是把被remove节点的前后连上
private void remove() {
before.after = after; //removeElement.before.after = remove.after
after.before = before; //removelement.after.before = remove.before
}
就这些,代码不多,核心还是LinkedHashMap。