在开发Android过程中,多少会涉及到缓存。例如加载网络图片,不能每次要显示某张网络图片,都要从网络下载,这样的话,不仅浪费用户流量,还可能会造成不好的体验。一般的做法都是先将图片加载到本地保存起来,下次还需要显示同一张图片,直接从内存中获取就行,无需通过网络。这就是缓存了。
我们都知道,Android的内存少的可怜,动不动就OOM,所以缓存需要一定的策略,如LRU,LFU,FOFI。
LRU(Least Recently Used),最近最少使用原则,也就是淘汰最长时间未使用的对象。如缓存的顺序为
A -> B -> C -> D -> A
假如已缓存满了,此时要假如一个E,根据最近最少使用原则,则会淘汰B,那么容器中只剩下ACDE。
我们先看一下怎么使用这个LruCache缓存数据:
int cacheSize = 4 * 1024 * 1024; // 4MiB
LruCache bitmapCache = new LruCache(cacheSize) {
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
}}
从源码分析LruCache,从属性看起:
// 保存数据的Map
private final LinkedHashMap map;
// 当前size,不一定是个数,从上面的例子可以看出,我们可以自定义size
private int size;
// 最大size,同上
private int maxSize;
// put个数
private int putCount;
// get没击中后,创建个数
private int createCount;
// 剔除的个数
private int evictionCount;
// get击中(根据key找到value)个数
private int hitCount;
// get没击中个数
private int missCount;
LruCache使用了LinkedHashMap保存数据。LinkedHashMap有个特性,就是保存的数据按访问顺序来排列的,举个例子就很清楚了:
public class Tester {
private static int MAX=5;
public static void main(String[] args) {
HashMap map = new LinkedHashMap(MAX/2,0.5f,true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest){
// 如果超过了MAX个,就允许删除最老一个
if(size() > MAX) {
return true;
}
return false;
}
};
map.put("a", "a");
map.put("b", "b");
map.put("c", "c");
map.put("d", "d");
map.put("e", "e");
map.put("c", "c");
for (Map.Entry entry : map.entrySet()) {
System.out.print(entry.getValue() + ", ");
}
System.out.println();
map.get("b");
for (Map.Entry entry : map.entrySet()) {
System.out.print(entry.getValue() + ", ");
}
System.out.println();
map.put("f", "f");
for (Map.Entry entry : map.entrySet()) {
System.out.print(entry.getValue() + ", ");
}
System.out.println();
}
}
打印结果为:
a, b, d, e, c,
a, d, e, c, b,
d, e, c, b, f,
LruCache正好利用了LinkedHashMap这个特性做到最近最少使用原则的。再看构造函数:
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap(0, 0.75f, true);
}
设置了最大size,并初始化了LinkedHashMap,我们结合一下LinkedHashMap构造函数了解初始化参数:
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
init();
this.accessOrder = accessOrder;
}
initialCapacity是初始化空间大小
loadFactor是加载因子,就是当他的size大于initialCapacity*loadFactor的时候就会扩容
accessOrder是否按照访问顺序排列数据
LruCache的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增加新值大小
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
// 如果之前有值,size减去旧值大小
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
// 提供子类释放旧的资源
entryRemoved(false, key, previous, value);
}
// 限制大小
trimToSize(maxSize);
return previous;
}
put方法先调用了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;
}
safeSizeOf调用了sizeOf方法计算:
protected int sizeOf(K key, V value) {
return 1;
}
sizeOf方法默认返回1,也就是LruCache默认计算大小的方式是,我们put一个数据,size加1,所以一般我们会重写这个方法,如:
int cacheSize = 4 * 1024 * 1024; // 4MiB
LruCache bitmapCache = new LruCache(cacheSize) {
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
}}
我们继续看put方法,计算完大小之后,然后调用map.put(key, value)方法把数据加入到LinkedHashMap中。如果之前有数据,也就是previous不为null,size要减去previous的大小。如果previous不为null,还调用了entryRemoved方法:
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
竟然是一个空的方法,也就是子类可以重写这个方法。根据官方解释,如果你的缓存值,需要显式释放的资源,你可以调用这个方法。明白了吧!!!继续put方法,最后调用了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;
}
// 如果当前size大于最大size,取出最老(不常)的一个
Map.Entry toEvict = map.eldest();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
// 从LinkedHashMap移除
map.remove(key);
// 减去移除数据的大小
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
从代码注释的分析来看,trimToSize方法是限制大小,如果当前size超过最大的size,剔除最近最不常用的数据。put方法分析完毕,一般的套路都是,有put就是get,我们看看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++;
}
/*
* 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创建新的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增加新创建的值大小
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
// 返回查找的值
return mapValue;
} else {
// 限制大小
trimToSize(maxSize);
// 返回新创建的值
return createdValue;
}
}
先从map中根据key找value,如果value不为空,直接返回value,结束查找。如果没找到,调用create(key)方法尝试创建一个,如果不为null,就保存到map中:
protected V create(K key) {
return null;
}
默认返回一个null对象,所以这个方法和entryRemoved方法一样,提供子类重写,意思是,如果我们获取不到key对应的值,重写之后,可以提供一个默认的值,并保存到map中。保存的过程中,会涉及到线程不同问题,还要判断一下,是不是已经有值了(如果你觉得:就是因为找不到value,才创建新的,为何还要判断map的key是否有值,那证明你线程同步还得学习一下)。如果有值,就不把新创建的加入到map中,并返回该值。如果没有值,就把心创建的加入map中,调用trimToSize重新限制大小,并返回新创建的值。
LruCache提供了remove方法删除缓存,代码:
public final V remove(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V previous;
synchronized (this) {
previous = map.remove(key);
// 如果删除成功,size减去移除数据的大小
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
// 提供子类释放资源
if (previous != null) {
entryRemoved(false, key, previous, null);
}
// 返回当前移除的对象
return previous;
}
还提供了清除所有缓存的方法:
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}
LruCache允许调用resize方法重新定义最大size:
public void resize(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
synchronized (this) {
this.maxSize = maxSize;
}
trimToSize(maxSize);
}
LruCache源码并不多,也比较容易理解。