Lru是一种常见的置换策略,并不是Android所特有的。它的特点是置换近期最久未被访问的数据,下面来看看Android是怎么实现LRU缓存策略的。
创建
LruCache的创建的基本方法如下。
LruCache<Bitmap,String> mCache = new PictureCache((int) (Runtime.getRuntime().maxMemory() / 1024 / 8));
这里的使用的单位是KB,Cache的大小为应用内存的1/8,来看看源码。
private final LinkedHashMap<K, V> map;
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);
}
代码很简单,可以看到缓冲的数据是用LinkedHashMap来存储的,LinkedHashMap是有序的Map,通过一个链表来存储元素。先来看看这里LinkedHashMap的特点。
它的构造方法传入了true,说明它会根据各元素的访问次数调整其在链表中的位置。访问的元素会被放到链尾,如此一来近期访问少的元素就会渐渐靠近链首的位置。这个特点恰好符合LRU缓存策略的需要。
存入
mCache.put(url,bitmap);
这是存储的基本方法,来看看它的源码。
public final V put(K key, V value) {
//1
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
//2
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
//3
entryRemoved(false, key, previous, value);
}
//4
trimToSize(maxSize);
return previous;
}
代码1先进行判空,合法输入才会继续。代码块2进入同步代码块,插入或更新缓存,如果是新存入的数据则把增加当前已用缓存大小。safeSizeOf方法是用来检查缓冲数据大小是否合法的,来看看它的代码。
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
如果缓存大小小于0,则不合法。还有一点需要注意,sizeOf方法默认返回1,指存入数据的大小,sizeOf返回的单位应该和LruCache存储单位一致,否则会导致错误。
代码3的entryRemoved方法是用来处理新旧数据的,默认是一个空方法。如果想要覆盖掉旧数据,则需要重写此方法。此外,有的数据由于是强引用,GC无法将其回收,我们也可以在这里对它们进行手动回收。
至此插入\更新数据就基本完成了,接下来到代码4,trimToSize方法是用来削减缓冲开销的。来看看做了什么。
private void trimToSize(int maxSize) {
//A
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!");
}
//1
if (size <= maxSize) {
break;
}
//2
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);
}
}
先进行合法判断,然后来到代码1,当前大小size小于缓冲容量则直接返回,也就是说只有在缓冲满了LruCache才会进行削减策略。刚刚说到LinkedHashMap会把近期访问的元素转移到链尾,近期访问少的元素渐渐靠近链首。代码2获取到链首元素也就是最近访问最少的,然后移除它,remove方法后面会分析。
整个方法是在一个死循环中进行的(代码块A),也就是说trimToSize方法会多次进行移除操作,直到size小于缓冲容量为止。
读取
bitmap = mCache.get(url);
缓存读取如上,看看get方法的源码。
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
//1
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.
*/
//2
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;
}
}
代码块1是同步代码块,读缓存就在里面完成,可见LruCache的访问操作是线程安全的。访问数据不为空则直接返回。否则到代码2创建缓冲数据。
create方法默认是返回null的。如果我们希望在访问为空时自动添加一个缓冲数据,则需要重写这个方法。接下的添加操作就和put方法差不多了。
移除
mCache.remove(url);
基本使用如上,来看看它的源码。
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;
}
代码很简单,先进行合法检查,把它从LinkedHashMap中移除,必要时再手动回收它的内存。
参考文章
https://www.jianshu.com/p/471e03a5d9bf
https://www.jianshu.com/p/ef22974673ea