Android 缓存 - LruCache
在开发Android过程中,多少会涉及到缓存。例如加载网络图片,不能每次要显示某张网络图片,都要从网络下载,这样的话,不仅浪费用户流量,还可能会造成不好的体验。一般的做法都是先将图片加载到本地保存起来,下次还需要显示同一张图片,直接从内存中获取就行,无需通过网络。这就是缓存了。
我们都知道,Android的内存少的可怜,动不动就OOM,所以缓存需要一定的策略,如LRU,LFU,FOFI。
LRU(Least Recently Used),最近最少使用原则,也就是淘汰最长时间未使用的对象。如缓存的顺序为
A -> B -> C -> D -> A
假如已缓存满了,此时要假如一个E,根据最近最少使用原则,则会淘汰B,那么容器中只剩下ACDE。
我们先看一下怎么使用这个LruCache缓存数据:
int cacheSize = 4 * 1024 * 1024; // 4MiBLruCache bitmapCache = new LruCache(cacheSize) { protected int sizeOf(String key, Bitmap value) { return value.getByteCount(); }}}
从源码分析LruCache,从属性看起:
// 保存数据的Mapprivate final LinkedHashMap map;// 当前size,不一定是个数,从上面的例子可以看出,我们可以自定义sizeprivate 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; // 4MiBLruCache 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源码并不多,也比较容易理解。
更多相关文章
- Android 中,应用程序需要的图片资源如何针对不同屏幕大小手机设计
- 关于“Android SDK manager中不出现完整Android版本安装包列表”
- Android关于apk版本更新方法
- Android之gallery 常见2种使用方法和3D效果总结
- Android中一张图片占用的内存大小
- [置顶] android中图片的三级cache策略(内存、文件、网络)之三:文件