简介
Android 中常常会用通过网络请求数据,为了节省流量、电量以及时间等等,一般会把得到的数据进行缓存。缓存分为内存缓存和文件缓存。Android 自带的内存缓存是 LRU
机制,也即是最近最少使用算法,对应的类是 LruCache
。要说它的原理,一句话概括就是使用了 LinkedHashMap
。本文具体分析 LruCache
源码的实现,其实还是比较简单的。
变量及构造方法
LruCache
的内部变量及构造方法如下:
private final LinkedHashMap<K, V> map;
private int size;
private int maxSize;
private int putCount;
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;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
在 LruCache
中,有一个 LinkedHashMap
变量,它就是实际存储缓存数据的。LinkedHashMap
继承自 HashMap
,但是增加了记住元素插入或者访问顺序的功能,这个是通过内部一个双向的循环链表实现的。
size
和 maxSize
变量分别表示当前缓存数据的大小以及缓存最大可使用的大小。下面的几个以 Count 结尾的变量是记录相应操作的命中次数。
LruCache
的构造方法接受一个 int 变量,用于指定缓存的最大使用量。构造方法中创建了 LinkedHashMap
, 它有三个参数。第一个是初始容量,第二个是加载因子,这两个都是 HashMap
中的概念 (Java HashMap源码分析)。第三个参数是一个布尔值,true
表示 LinkedListMap
按照元素的最近访问排序,false
则表示按照元素的插入次序排序,LruCache
实现的是最近最少访问,所以这里指定为 true
。
put 和 get
LruCache
创建后最常用的两个操作就是 put
和 get
了。先看 put
的代码:
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);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
如果 key 或者 value 为空会抛出异常,否则在同步块中进行添加操作。首先是 putCount
加一,然后调用 safeSizeOf
方法增加 size
,接着把数据放到 map
中,如果这个 key 已经存放了数据,那么应该减去这条数据的大小,因为它已经被覆盖调了。同步块结束后,如果确实覆盖了数据,会调用 entryRemoved
,这个方法默认是空,什么也没做,我们自己创建 LruCache
时可以选择重写。最后还需要调用 trimToSize
,这个方法用来防止数据超出 maxSize
。
上面在计算 size
大小时调用了 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;
}
protected int sizeOf(K key, V value) {
return 1;
}
这个方法又调用了 sizeOf
返回数据的大小,如果小于 0 抛出异常,否则就返回。sizeOf
这个方法是我们熟悉的,一般使用 LruCache
都会重写这个方法返回每条数据的实际大小。为什么要重写呢? 因为这个方法默认的实现是返回 1。这样的话,size
相当于记录的是缓存数据的条数,而这可能并不是我们想要的。
下面再看看 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) {
break;
}
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);
}
}
内部是一个无限循环,删除 map
里面最久未使用的,然后更新 size
,如果 size
小于 maxSize
就跳出循环。
put
相关的代码就是这样,现在看看 get
的实现:
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
//找不到就创建一个value
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;
}
}
protected V create(K key) {
return null;
}
key 依然不能为空,然后就是从 map
中取数据,递增hitCount
,最后直接返回数据。这是成功找到缓存的情况,如果找不到还会执行下面的代码。下面的逻辑是调用 create
创建 value。create
需要我们自己重写,默认返回 null
,所以默认情况下找不到缓存就返回 null
。 如果重写了 create
那么接着会把新建的数据加入 map
,并且增加 size
,执行 trimToSize
等操作。
其它操作
除了 get
和 put
,其它还有一些操作。比如 evictAll
用于清除所有缓存,size()
返回 size
大小,一系列的以 Count 结尾的方法用于返回 hitCount 等计数值的大小。这些代码都比较简单,没什么好说的。
总结
LruCache
实现了数据的内存缓存,可以看出整体思路并不是很复杂,关键在于使用了 LinkedHashMap
。
如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)