LruCache原理

对于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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值