内存缓存技术LruCache原理分析
1.告别软引用
最近几天在学习缓存技术,缓存无非就是利用空间换时间,以达到应用流畅的目的。在三级缓存中,最重要的就是内存缓存,因为cpu与内存的直接读取关系,我们可以让图片等数据保存在内存中,保证用户下次读取不用无聊的等待。同时,内存又是很珍贵的空间,所以我们不能无限制的使用内存。
一般情况下,我们都会想到使用软引用或者弱引用,比如利用HashMap以键值对的方式存储经软引用封装的Bitmap对象:
private HashMap<String,SoftReference<Bitmap>> mCache = new HashMap<String, SoftReference<Bitmap>>();
不同于强引用在系统内存不足时垃圾回收器不会进行回收,软引用在系统内存不足时垃圾回收器会考虑回收。
然而,现在这种方式并不推荐:
在Android2.x+(忘了具体哪个版本 TvT)之后, Android在内存够用时也会进行软引用对象的回收,所以这种方式就不管用了。
2.最近最少原则
Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃,这时就可以用LruCache来缓存图片。
Lru = Least recently used(最近最少使用),开辟一块固定大小的内存,把图片存放在内存中,当图片对象越来越多,内存快装满的时候,在一推缓存好的图片中把最近最少使用的图片给剔除,这样就有个好处,通过动态的剔除图片控制了缓存空间大小,缓存的同时不会造成内存溢出。
3.LinkedHashMap分析
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
/** Size of this cache in units. Not necessarily the number of elements. */
private int size;//已经存储的大小
private int maxSize;//规定的最大存储空间
private int putCount;
private int createCount;
private int evictionCount;
private int hitCount;
private int missCount;
LruCache的内部定义了一个LinkedHashMap,这个LinkedHashMap本质上也是个HashMap,那有人会问了,为什么不用HashMap而用LinkedHashMap?
解释下上面的问题,LinkedHashMap是比HashMap多了一个链表的结构。与HashMap相比LinkedHashMap维护的是一个具有双重链表的HashMap,LinkedHashMap支持2中排序一种是插入排序,一种是使用排序,最近使用的会移至尾部例如 M1 M2 M3 M4,使用M3后为 M1 M2 M4 M3了,LinkedHashMap输出时其元素是有顺序的,所以利用这一特性,我们就可以实现把最近最多使用的图片对象放在尾部,最少使用的就自然而然到了顶部,内存不足时剔除就行。而HashMap输出时是随机的,如果Map映射比较复杂而又要求高效率的话,最好使用LinkedHashMap,但是多线程访问的话可能会造成不同步,所以要用Collections.synchronizedMap来包装一下,从而实现同步。
HashMap是java的数据结构,而LinkedHashMap是Android特有的数据结构,在开发中尽量使用Android特有的数据结构,比如SparseArray和LinkedHashMap等等,他们都有更好的性能。
4.重写sizeof
LruCache已经封装在官方api下了,我们使用v4包下的LruCache:
//开辟的缓存大小最好为应用可用内存大小的1/8
int maxSize = (int) (Runtime.getRuntime().maxMemory() / 8);
//传入一个参数为缓存空间的总大小,和两个泛型为键值对,值为图片对象
LruCache mCache = new LruCache<String, Bitmap>(maxSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return (value.getRowBytes() * value.getHeight());
}
};
使用LruCache要重写里面的sizeof方法,返回的是一个图片能占多大内存,图片占内存=图片的行字节×高(value.getRowBytes() * value.getHeight()),默认是1。
5.trimToSize
LruCache有put和get两个公共方法以键值对的方式存取对象到LinkedHashMap里,存取的同时size += safeSizeOf(key, value);和size -= safeSizeOf(key, previous);控制size的大小
private 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;
}
// BEGIN LAYOUTLIB CHANGE
// get the last item in the linked list.
// This is not efficient, the goal here is to minimize the changes
// compared to the platform version.
Map.Entry<K, V> toEvict = null;
for (Map.Entry<K, V> entry : map.entrySet()) {
toEvict = entry;
}
// END LAYOUTLIB CHANGE
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
这个方法是Lru的核心,put的时候,会判断当前总大小size是否小于开发者定的maxSize,如果小于就break循环完事,如果大于的话,就remove那个map中最近最少使用对象,再把当前size减少。
总结:
通过这个size(当前对象集合所使用的内存大小)控制一旦发现size大于开发者定的maxsize的话,删除一个对象,而这个对象是遍历出来的第一个(这个就是最近最少使用的一个),通过while循环,如果可用size还不够的话就接着删除下一个对象。通过这样的方式控制size的大小总是小于maxsize,从而达到控制内存大小。