LruCache——解决OOM的利器

简介

LruCache是Android 3.1的时候出现的,一般我们为了兼容低版本会使用v4包下的。LruCache是一种缓存策略,持有的是强引用,但是会控制在一个峰值下。它内部维护了一个队列,每当从中取出一个值时,该值就移动到队列的头部。当缓存已满而继续添加时,会将队列尾部的值移除,方便GC。LruCache用于内存缓存,在避免程序发生OOM和提高执行效率有着良好表现。

LRU算法

和名字一样,LruCache的实现正是基于LRU(Least Recently Used)算法。最近最少使用,我理解的就是最久远的最少使用先被淘汰。下图展示了LRU算法的核心思想,是最常用也是比较简单的一种:
LruCache——解决OOM的利器_1.png

假设一个队列的最大容量是5,那么新进的元素会被添加到头部,当队列已满时继续添加会移除尾部的元素。值得注意的是,如果有一个不在队头的元素C又一次插入到队列,因为队列中已经存在C,则不会重复插入,而是将C元素移动到头部,相当于它的存在优先级当前是最高的。

LinkedHashMap

查看LruCache的源码很容易就发现,只有一个容器类,就是LinkedHashMap。正好LinkedHashMap是一个双向链表的数据结构,分为访问顺序和插入顺序。而LruCache只提供了一个有参构造函数:

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);
}

在这里初始化了LinkedHashMap,并且是按访问顺序排序。也就是说,遍历该队列的entrySet,最先输出的总是最早被插入并且最近没有被访问(操作)过的。最后被访问的元素一定是作为队尾输出的。要注意这里的队头队尾,和LruCache描述的队列以及上图中的队列的区别,比较容易让人蒙蔽。

LruCache的初始化

以下是比较重要的几个成员变量及代表的含义:

private int size;           //当前缓存的大小
private int maxSize;        //最大可缓存的大小

private int hitCount;       //命中缓存的次数
private int missCount;      //丢失缓存的次数

LruCache的构造函数中传入参数为可缓存的最大容量并赋值给maxSize,如果在初始化该LruCache对象时没有重写其sizeOf方法,那么maxSize就代表了其内部的LinkedHashMap可以存储的最大键值对数量。因为sizeOf默认返回1,代表生产了一个键值对。注意只有maxSize和sizeOf返回值是同一个单位制缓存的判断才有意义。

put

LruCache插入元素(缓存值)的方法:

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 += safeSizeOf(key, value);
        previous = map.put(key, value);
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }

    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }

    trimToSize(maxSize);
    return previous;
}
  1. 异常判断说明LruCache不允许键或值为null的操作。
  2. 在插入元素前会调用一次sizeOf,前面已经说过默认返回1,但一般我们会根据实际需要重写。比如用LruCache存储的value为File,那么sizeOf返回的就应该是当前对应该key的文件大小。
  3. 相应的size也要完成自增长,因为当前缓存增加了,并且将对应的key-value插入到链表中。
  4. 二次检查,如果该key已经存在链表中,此时新的value覆盖后,size要减去之前的value所占用的大小。
  5. 上面的操作都是同步的,为了在多线程场景下保证size的准确性,否则缓存策略失效
  6. 如果是覆盖了旧的value,LruCache对外提供了一个空方法entryRemoved
  7. 调用trimToSize,保证缓存不溢出。

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 || map.isEmpty()) {
                break;
            }

            Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
            key = toEvict.getKey();
            value = toEvict.getValue();
            map.remove(key);
            size -= safeSizeOf(key, value);
            evictionCount++;
        }

        entryRemoved(true, key, value, null);
    }
}

该方法每插入一次元素就会被调用一次。整个方法就是一个无限循环,判断当前缓存大小不大于最大容量则结束循环。否则就取出LinkedHashMap的entrySet的头部,也就是最早被插入且最近未被访问过的键值对并删除,更新size。重复此步骤直到缓存<=最大容量。不得不说利用访问顺序的LinkedHashMap的特性完成LRU缓存,非常巧妙。

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.
     */

    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. key不可以为null
  2. 如果map中存在与key相对应的value,则返回该value,并且缓存命中数+1。不存在,则缓存丢失数+1
  3. 不存在的话会尝试根据该key创建一个value。创建方法默认返回null,需要自己实现。

其它

除此之外,LruCache还提供了手动清除指定缓存remove(K key),清除所有缓存evictAll()等方法供使用者调用。弄明白LinkedHashMap就很容易弄懂LruCache的实现了。

猜你喜欢

转载自blog.csdn.net/wzhseu/article/details/81745799
OOM
今日推荐