对于Android的内存优化 ,特别是图片的优化,当使用内存缓存时,一般很容易想到是用LruCache,下面分析下LruCache的使用原理。
在分析原理之前,请务必注意分析的LurCache源码的是哪个包下的,不同Android api下的LruCache的源码有些不同,主要是不同版本的LruCache类中的trimToSize方法写的不同,导致笔者在分析时,感觉其中的逻辑矛盾,走了不少弯路。
下面贴出能用的LruCache源码的导入方式:
import android.support.v4.util.LruCache;(能用)
import android.util.LruCache;(不能用这个)
下面一段话是官方文档对LruCache的简单描述:
A cache that holds strong references to a limited number of values. Each time a value is accessed, it is moved to the head of a queue.When a value is added to a full cache, the value at the end of that queue is evicted and may become eligible for garbage collection.
翻译过来就是:
一个包含有限数量强引用的缓存,每次访问一个值,它都会被移动到队列的头部,将一个新的值添加到已经满了的缓存队列时,该队列末尾的值将会被逐出,并且可能会被垃圾回收机制进行回收。
从官方文档对LruCache的描述可以知道,LruCache就是一个容器,并且这个容器的空间是有限的,将一些对象存放到这个容器中缓存起来,如果要在次使用这个对象,就从这个容器中找,如果容器中还保存了这个对象,就直接使用这个对象,这样就提高了效率。在访问了这个容器的对象后,LruCache内部会将这个对象移动到链表的尾部(如果将LurCache看成队列,则会将这个对象移动到队列的头部)由于这个容器的空间是有限的,所以,每次向容器中添加对象后,会判断当前LruCache中所有缓存的对象的内存大小是否超过了LruCache能够容纳的最大内存大小,如果超过了,会按照LRU算法,将链表的头部(如果将LurCache看成队列,则是队列末尾)的对象清除,这个队尾的对象其实就是最近最少使用的对象,这个过程会一直重复,直到LruCache中所有的对象的内存大小小于或等于LruCache的允许的最大内存大小。
下面看看LruCache的使用:
private void testLruCache() {
int maxSize = 16;//指定LruCache最大缓存容量是 16 Byte
LruCache<Integer,Integer> lruCache = new LruCache<Integer, Integer>(maxSize){
@Override
protected int sizeOf(Integer key, Integer value) {
return Integer.BYTES;//每个缓存的元素占用的内存大小
}
};
lruCache.put(1,1);
lruCache.put(2,2);
lruCache.put(3,3);
lurCache.get(1);
lruCache.put(4,4);
lruCache.put(5,5);
Integer integerValue = lruCache.get(1);
LogUtil.i(integerValue+"");
Integer integerValue2 = lruCache.get(2);
LogUtil.i(integerValue2+"");
}
打印结果:
11-15 11:12:50.911 6564-6564/test.cn.example.com.androidskill I/MY_LOG: -->>1
11-15 11:12:50.916 6564-6564/test.cn.example.com.androidskill I/MY_LOG: -->>null
从打印结果,可以看出,当存入了key为1,2,3的对象后,接着访问了key为1的对象,此时,会将key为1的对象
放入到链表的尾部,在存入key为4的对象后,链表中的对象的顺序是(链表头依次到链表尾)2,3,1,4,当存入key
为5的对象后,通过计算发现LruCache中缓存的所有对象的内存总大小超过了LruCache指定的最大容量,此时,会将链表头部(如果将LruCache看成队列,则是队列的队尾)key为2的对象先移出链表,移除后会继续这个过程,直到LruCache中的所有对象的内存占用量小于或等于LruCache允许的最大内存数,完成这个过程后,这时链表中的对象的顺序是3,1,4,5,所以在获取key为1的对象时,获取的值是1,获取key为2的对象时,返回的是null。
下面通过LruCache的源码来分析上面的整个过程:
首先看LruCache的构造方法:
public class LruCache<K, V> {
...
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);
}
...
}
这方法内部,主要对传入的maxSize的值进行保存,并创建了一个LinkedHashMap对象,注意,在创建LinkedHashMap对象时,第三个参数传入的是true。这里要说明下,这个参数的含义,如果这个参数传入的是false,表示存入LinkedHashMap中的元素是按照插入的顺序排序,如果传入的是true,表示存入的元素是按照访问顺序排序的。
下面看看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++;
// 关键代码1
size += safeSizeOf(key, value);
//关键代码2
previous = map.put(key, value);
if (previous != null) { // 关键代码3
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
//关键代码 4
trimToSize(maxSize);
return previous;
}
这个方法内部主要做了以下几件事:
1.在关键代码1处,先将添加的元素的内存大小计算出来后,累加到size这个全局变量中,这个size代表了当前LruCache中的元素的总的内存大小
2.在关键代码2处,通过LinkedHashMap类型的对象map来存储这个key和value。
3.在关键代码3处,会判断这个存入的对象是否存在,如果存在,则将之前累加到size的值给减掉
4.在关键代码4处,调用trimToSize()方法来判断map中的所有对象所占用的内存是否超过了maxSize,如果超过就删除链表头端的对象,删除了这个对象后再次重复这个过程,直到LinkedHashMap中的所有对象占用的内存总数小于maxSize为止。
下面是trimToSize()方法的具体实现:
public void trimToSize(int maxSize) {
while(true) {
Object key;
Object value;
synchronized(this) {
if (this.size < 0 || this.map.isEmpty() && this.size != 0) {
throw new IllegalStateException(this.getClass().getName() + ".sizeOf() is reporting inconsistent results!");
}
if (this.size <= maxSize || this.map.isEmpty()) {
return;
}
//获取链表头的节点
Entry<K, V> toEvict = (Entry)this.map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
//删除链表头节点
this.map.remove(key);
this.size -= this.safeSizeOf(key, value);
++this.evictionCount;
}
this.entryRemoved(true, key, value, (Object)null);
}
}
说完了LruCache的put方法后,下面来看看get方法的具体实现:
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//关键代码1
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
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;
}
}
这个方法内部的关键代码1处,会调用LinkedHashMap的get方法,下面看看这个方法
#LinkedHashMap.java
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{
...
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
//关键代码
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
...
}
这个方法内部,会根据accessOrder的值来决定afterNodeAccess(e)方法是否执行,由于在构造LruCache时,这个值传入的是true,所以这里的这个if判断是成立的,这样afterNodeAccess(e)方法就得到执行,这个方法内部会将要访问的对象插入到链表的尾端。所以,LruCache的get方法不仅获取了这个key相应的值,还将这个key对应的节点插入到了链表的尾部。这样就达到了每次访问LruCache中的对象就会将这个对象所对应的节点移动到链表尾部,在LruCache的容器中的缓存的所有对象的内存总数超过了LruCache允许的最大内存数后,就会将链表头的节点移除,这样就达到了就近最少使用的对象被删除的效果。
总结:
LurCache的核心原理是,包装了一个LinkedHashMap,不过这个LInkedHashMap在构造时,传入的排序方式的是按照访问顺序排序。当缓存满了之后,LruCache 是最近最少使用的元素会被移除,当使用 get() 访问元素后,会将该元素移动到 LinkedHashMap 的尾部。
参考:
LruCache 的使用及原理
https://www.jianshu.com/p/8215107977a9
Android 源码之LruCache
https://www.jianshu.com/p/780dda1e70f0
图解LinkedHashMap原理
https://www.jianshu.com/p/8f4f58b4b8ab