前言
在开发android程序过程中,利用缓存,能够更好的提升用户体验。也许大家没有自己写过缓存的控制,但是在我们日常使用的一些框架中都会用到缓存。例如Glide图片加载框架,这次我们从源码的角度看一下android缓存机制LruCache。
LRU(Least Recently Used) “最近最少使用”
一、LruCache用法
我们这里说的LruCache是内存缓存,这篇文章我们不涉及磁盘缓存。
我们要想用缓存,首先我们得先知道我们的app可以利用的最大内存是多少,从而确定用多少的内存作为缓存。
int maxMemory = (int) (Runtime.getRuntime().totalMemory() / 1024);
先获取到最大的内存,不同的手机可分配到的内存是不一样的。我们这里以可使用内存的八分之一作为缓存的最大内存。如果你的app涉及的缓存比较多(比如快速加载大量图片等)可以适当提升缓存大小比例。
//缓存大小为最大内存的8分之1,我们这以缓存bitmap为例子
int cacheSize = maxMemory / 8;
LruCache imgCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
重写sizeOf方法,此方法返回值为 你每个放入内存的对象的大小,因为ruCache的构造方法传入的类时Object 所以LruCache不知道要缓存的是什么类型对象,不知道计算该类型大小的方法。所以需要自己提供该对象大小
我们主要用到的方法为put 和get方法为向内存中放数据和取数据
二、源码解析
我们先来看一下LruCache的构造方法
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
//从这可以看出 LruCache内部为一个LinkedHashMap
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
LinkedHashMap 有三个构造函
第一个 0为默认该hashmap的初始大小,如果开始知道我们要用的map大概多大可以直接指定大小,因为开始比较小后续会扩容,扩容是有时间开销的,所以开始可以指定大小以减少扩容开销
第二个 0.75f为装载因子 我们的map是有大小的 当我们map中放的数据与map现在总大小的比例大于这个装载因子时 map就会进行扩容
第三个 也是实现LRU的关键点 这个参数 为true时map中的顺序为访问顺序 为false时是插入顺序 也就是说如果为true我们访问了map中的数据 就会把这个数据放到队列的最后面(最近最少使用的也就是要淘汰的是放在队列头部的)
我们知道了LinkedHashMap的构造函数的参数后,再来看一下 第三个参数是如何影响读取顺序的呢主要方法是在LinkedHashMap 的get方法
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)//这里accessOrder 就是构造函数中第三个参数 赋值后的参数 可以看到为true的话会执行 下面这个方法
afterNodeAccess(e);
return e.value;
}
在看一下afterNodeAccess方法 也就是说如果设置为true的话 在每次get得到某个数据后 都会把该数据放到队列的末尾(因为淘汰的话是从队头淘汰)
//这里源码上也写了注释 把该node数据 移到末尾 ,因为是双向链表,所以移动逻辑还比较简单
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMapEntry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
好,到这我们知道了LruCache的内存是linkedHashMap实现的并且可以实现为访问了最近的数据后就把该数据放队尾这么一个操作。那么LruCache怎么实现的淘汰呢。我们来看一下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++;//一共put过多少次 计数用
size += safeSizeOf(key, value); //添加该value后 当前数据一共多大
//向map中添加数据 如果map中已经包含该数据了会把该数据返回回来,如果没有该数据则返回null
previous = map.put(key, value);
if (previous != null) {
//如果map中已经包含该数据则把刚才 加上的该value的大小在当前大小中减去
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
//实现超出缓存的点在这个方法中 我们来看这个
trimToSize(maxSize);
return previous;
}
上面safeSizeOf这个方法其实就是在内部调用的我们在初始化LruCache后重写的sizeOf方法,我们来着重看这个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中的最早进入的数据,也就是队头数据。
Map.Entry<K, V> toEvict = map.eldest();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
//在map中remove掉该数同时把当前的把当前的内存减掉该数据的内存
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
//调用这个回调方法 这个方法的实现是在Lrucache的时候重写的 也就是在元素被淘汰的时候 可以让开发者有感知 从而可以对被淘汰的数据进行处理。
entryRemoved(true, key, value, null);
}
}
从上面的put的代码中可以看到 在放入一个数据到Lrucache中的时候如果超出了maxsize了 就会进行淘汰,淘汰后还会调用entryRemoved 我们可以重写该方法处理数据。
下面我们看一下Lrucache的get方法
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//直接返回的linkedmap的get方法返回的数据。
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.
*/
//如果没拿到数据 在这里返回null 这个creat方法直接返回的null
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;
}
}
从上面代码可有看出get方法基本上算是调用的 linkedHashMap的get方法,从而在拿到数据后调用了afterNodeAccess 方法。也就是上面说的把当前数据移动到数据队尾。
三、总结
LruCache 内部通过LinkerHashMap实现,利用LinkedHashMap的访问数据后把该数据放在队尾的特性,实现最近最少使用的对象放到了hashmap的队头。从而知道了淘汰的时候淘汰哪个数据。
好多解析都在代码的注释中,大家看的时候可以在androidstudio中结合自己的源码看,会有更大收获。