秒懂 LruCache

目的

在 Android 开发中,我们需要避免程序占用过多的内存资源或者存储空间,比如网络加载图片下载文件等,当缓存大小达到一定值的时候我们需要从缓存中释放空间存放新的缓存,LruCache 就是常用的用于管理缓存的类。

LruCache 缓存算法

Lru: Least Recently Used
原理: 当缓存大小大于最大值时,优先抛弃缓存中最近最少使用的缓存,直到当前缓存大小小于等于最大值。

LruCache LRU 实现

利用 LinkedHashMap 可以根据元素调用排序的特点,提供 get() put() remove() 等方法来操作缓存。


源码解析

构造方法:

// LruCache.java
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);
}

构造方法的核心就是创建了一个初始容量为 0 ,默认 loadFactor = 0.75f,根据元素访问排序的 LinkedHashMap.

// LruCache.java
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; // 当前缓存大小,不一定跟 map 中的键值对数相同
    private int maxSize; // 最大缓存大小

    private int putCount; // 标记 put() 被调用次数
    private int createCount; // 标记调用 create() 且返回不为 null 的次数
    private int evictionCount; // 标记 map 中被抛弃的值的数量
    private int hitCount; // 标记调用 get() 时存在值的次数
    private int missCount; // 标记调用 get() 时不存在值的次数
    
    ...

    public synchronized final int size() {
        return size;
    }

    public synchronized final int maxSize() {
        return maxSize;
    }

    public synchronized final int hitCount() {
        return hitCount;
    }

    public synchronized final int missCount() {
        return missCount;
    }

    public synchronized final int createCount() {
        return createCount;
    }

    public synchronized final int putCount() {
        return putCount;
    }

    public synchronized final int evictionCount() {
        return evictionCount;
    }
}

核心方法:

1.get()

// LruCache.java
public final V get(K key) {
    if (key == null) { // 非空判断
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
        // 从缓存 map 中获取 mapValue
        mapValue = map.get(key);
        // 非空判断
        if (mapValue != null) {
            // 不为空,累加 hitCount 并返回 mapValue
            hitCount++;
            return mapValue;
        }
        // mapValue 为空,累加 missCount
        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.
     *
     * 创建一个 value。这里需要消耗点时间,map 有可能在 create() 结束的时候
     * 发生变化。并发情况下,如果在执行 create() 的时候有别的 value 添加到 map 中
     * 的时候,会保留别的 value 在 map 中并且丢弃 createdValue.
     */
    V createdValue = create(key);
    // 默认情况下返回 null
    if (createdValue == null) {
        return null;
    }

    // 同步锁代码块
    synchronized (this) {
        // 累加 createCount
        createCount++;
        // 把 createdValue 放进 map
        // LinkedHashMap.put() 返回的是该 key 原本的 value (原本不存在时返回null)
        // 无论 value 是否为 null createdValue 依然会添加到 map 中
        mapValue = map.put(key, createdValue);
        
        if (mapValue != null) {
            // 不为空则有别的 value 已经添加在 map 中,且重新把这个 value 放进 map 中
            // There was a conflict so undo that last put
            map.put(key, mapValue);
        } else {
            // 添加成功后根据 createdValue 的大小添加到 size
            size += safeSizeOf(key, createdValue);
        }
    }

    if (mapValue != null) { // 已经存在别的 value
        // 回调 entryRemoved() 
        entryRemoved(false, key, createdValue, mapValue);
        // 返回原本存在的 value
        return mapValue;
    } else { // 添加成功
        // 缩短缓存大小直到小于最大缓存值
        trimToSize(maxSize);
        // 返回创建的 createdValue
        return createdValue;
    }
}

protected V create(K key) {
    return null;
}

private int safeSizeOf(K key, V value) {
    int result = sizeOf(key, value);
    if (result < 0) {
        throw new IllegalStateException("Negative size: " + key + "=" + value);
    }
    return result;
}

protected int sizeOf(K key, V value) {
    return 1;
}

protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}

直接根据 key 从缓存 map 中直接获取 value,并进行非空判断:

  • 非null : 直接返回 value,结束 get() 。
  • null : 调用 create() 创建一个值若为 null 直接返回 null 结束 get(),不为 null 就调用 map.put() 到缓存中,由于存在并发的情况,所以有可能在 create() 执行过程中已经有别的 value 对应同一个 key 添加到 map 中的情况,所以根据判断 put() 返回值是否为空可以判断 map 中是否已经存在 key 对应的值 :
    • null: 创建的 createValue 添加成功,调用 safeSizeOf() 判断 createValue 的大小并累加到 size上,然后调用 trimTosize() 使缓存大小小于maxSize,最后返回 createValue。
    • 非null: map 中已存在别的值 mapValue ,重新把 mapValue 放进 map 中,然后调用回调方法 entryRemoved(),最后返回 mapValue。

在 get() 中有几个 LruCache 提供给我们根据业务需求重写的方法:

1.create()

默认直接返回 null,根据业务需求我们可以在发现 key 对应的值不存在的时候创建一个值放进缓存中。

2.sizeOf()

默认值为1,根据需求我们可以设置每个值的缓存大小,比如图片我们可以返回图片占用的内存大小。

3.entryRemoved()

默认空实现,每当有 value 从 map 中被移除或者被抛弃的时候会调用。

2.put()

// LruCache.java
public final V put(K key, V value) {
    if (key == null || value == null) { // 非空判断
        throw new NullPointerException("key == null || value == null");
    }

    V previous;
    synchronized (this) { // 同步锁代码块
        // 累加 put() 调用次数
        putCount++;
        // 累加缓存大小
        size += safeSizeOf(key, value);
        // 添加到 map 中
        previous = map.put(key, value);
        // 如果该 key 对应的 value 之前已存在,
        // 当前缓存大小就需要减掉之前的 value 大小 
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }

    // 如果该 key 对应的 value 之前已存在,
    // 调用回调方法 entryRemoved()
    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }
    // 最后调整缓存大小
    trimToSize(maxSize);
    // 返回之前的 value 值,若 value 之前不存在则为 null
    return previous;
}

放进缓存,并返回调用 put() 之前缓存中该 key 对应的 value。

3.trimToSize()

调整缓存大小直到小于设置的最大值为止。

// LruCache.java
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) { // 若末尾值为 null,跳出循环方法结束
                break;
            }
           // 获取 key
            key = toEvict.getKey();
            // 获取 value
            value = toEvict.getValue();
            // 从 map 中移除
            map.remove(key);
            // 从缓存大小中减去该值的大小
            size -= safeSizeOf(key, value);
            // 累加 evictionCount
            evictionCount++;
        }
        // 调用回调方法 entryRemoved
        entryRemoved(true, key, value, null);
    }
}

每次循环都移除调用顺序末尾的缓存,直到当前缓存大小小于设置的最大值。

4.remove()

根据 Key 从 map 中移除缓存。

// LruCache.java
public final V remove(K key) {
    if (key == null) { // 非空判断
        throw new NullPointerException("key == null");
    }

    V previous;
    synchronized (this) {
        // 从 map 中移除,并获取移除之前的 value
        previous = map.remove(key);
        if (previous != null) { 
            // 若移除之前值不为 null,当前缓存大小需要减去 previous 大小
            size -= safeSizeOf(key, previous);
        }
    }

    if (previous != null) {
        // 调用回调方法 entryRemoved
        entryRemoved(false, key, previous, null);
    }
    // 返回移除前的值,若移除前值不存在则返回 null
    return previous;
}

其他方法:

1.resize()

重置 LruCache 的最大缓存大小。

public void resize(int maxSize) {
    if (maxSize <= 0) {
        throw new IllegalArgumentException("maxSize <= 0");
    }

    synchronized (this) {
        this.maxSize = maxSize;
    }
    // 重新赋值 maxSize 后要调整令 size <= maxSize
    trimToSize(maxSize);
}

2.snapshot()

获取一个复制当前缓存 map 的 LinkedHashMap。

public synchronized final Map<K, V> snapshot() {
    return new LinkedHashMap<K, V>(map);
}

3.evictAll()

清除所有缓存。

public final void evictAll() {
    // 直接调用 trimToSize(-1) 则最后缓存大小会变成 0
    trimToSize(-1); // -1 will evict 0-sized elements
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352

推荐阅读更多精彩内容