Android中的LruCache的原理和使用
LruCache,虽然很多文章都把LRU翻译成“最近最少使用”缓存策略,但Android中的LruCache真的如此吗?
答案是No,它只做到了控制“最近使用”!
原理
数据结构
LruCache采用LinkedHashMap作为存储的数据结构,那么为什么是LinkedHashMap?
LinkedHashMap特性简介
- LinkedHashMap基于HashMap,具有HashMap高效查找、自动扩容等特性
- 在HashMap基础上,增加了一个双向链表存储K-V对、实现了自己的遍历器LinkedHashIterator,默认情况下可以做到根据数据插入顺序有序地遍历
- 提供重载构造方法供外部控制accessOrder,以实现根据访问顺序有序地遍历
初始化
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap(0, 0.75f, true);
}
LruCache的构造方法如上,可见LruCache初始化时就使用了上面提到的LinkedHashMap的第三点特性,即在数据结构层面实现了“最近使用”。
存储
当调用put方法添加/设置存储内容时,LruCache依次做了这么几件事:
- 判空,即不允许key/value为null
- 总容量size增加上计算传入的K-V大小
- 将传入的K-V存入LinkedHashMap
- 如过LinkedHashMap中已存在相同K,总容量size减去替换掉的K-VOld大小
- 通知VOld被替换(子类实现entryRemoved以监听)
- 比较总容量size和最大容量maxSize,若大于maxSize则循环移除LinkedHashMap头结点(即最久未被访问的结点)直至size小于等于maxSize
获取
当调用get方法获取存储内容时,LruCache依次做了这些事:
- 判空
- 从LinkedHashMap中取出与K对应的V值并返回。如果子类未实现create方法以达到当缓存未命中时创建并存入新V的话,返回null,get流程结束。
- 通过create创建VNew,并尝试把VNew存储到LinkedHashMap中,流程类似存储过程,不同之处在于当K冲突时,会舍弃掉新创建的VNew。不要奇怪为什么明明上面通过K取V的时候没取到,这里却会K冲突,因为LruCache为了性能考虑(防止子类自定义的create方法耗时过长影响get方法执行性能),只对从LinkedHashMap中取值的过程做了同步处理,这样在多线程的情况下就可能出现A线程在create的时候,B线程已经将K-VB存入了map。
- 返回上面创建的VNew或者VB
使用
用LruCache实现一个简单的图片缓存
class LruImageCache extends LruCache {
private static final String TAG = "LruImageCache";
private static final int DEFAULT_CACHE_SIZE = 20 * 1024 * 1024;
public LruImageCache() {
this(DEFAULT_CACHE_SIZE);
}
public LruImageCache(int maxSize) {
super(maxSize);
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
Log.d(TAG, "cache removed: " + key);
}
@Override
protected Bitmap create(String key) {
// 从本地、网络获取图片
return loadImageFromIO(key);
}
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getAllocationByteCount();
}
}
//使用
Bitmap b = LruImageCache.get("http://image-path");