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.
If your cached values hold resources that need to be explicitly released, override entryRemoved(boolean, K, V, V).
If a cache miss should be computed on demand for the corresponding keys, override create(K). This simplifies the calling code, allowing it to assume a value will always be returned, even when there's a cache miss.

LruCache是存储了优先数量的强引用缓存,每次访问一个值的时候,会将其移动到队列的头部,当一个值添加到已经满的队列的时候,会将队列尾部的元素移除掉,让GC回收。
如果缓存的值明确的要知道已经释放,需要重写entryRemoved(boolean ,K,V,V)方法,做一些自己的处理。
如果在缓存中没有找到一个对应的值,可以通过create(K),简化了调用代码,允许即使是没有找到对应值的情况下能够返回一个值。

成员变量和构造方法

public class LruCache<K, V> {

        // LruCache 的核心 LinkedHashMap
    private final LinkedHashMap<K, V> map;
    
    // 当前缓存大小
    private int size;
    
    // 最大缓存大小
    private int maxSize;
    
    // 插入次数
    private int putCount;
    
    // 创建次数,只有重写 create(K) 方法的时候会改变
    private int createCount;
    
    // 移除数据次数,缓存满的时候,插入新数据的时候,移除旧数据的时候,会改变这个值.
    private int evictionCount;
    
    // 命中次数,也就是 get 查找到元素的次数
    private int hitCount;
    
    // 未命中次数,也就是 get 没查找到元素的次数
    private int missCount;
    
    /**
     * @param maxSize 如果没有重写 sizeOf 方法,maxSize 就是缓存中元素的最大个数
     *                如果重写了 sizeOf 方法,则 maxSize 就是所有缓存元素大小(也就是每个元素乘以自身大小的总和)
     */
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        // 创建了一个默认容量 默认负载因子 ,允许访问排序的 LinkedHashMap.
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
}

上面的最主要的就是设置maxSize以及内部的定义的允许访问排序的LinkedHashMap。
maxSize在重写sizeOf()方法的情况下,代表的就是我们每个元素乘以自身大小之后累加的允许最大值。
LinkedHashMap的对应map则实现LruCache的核心,如果在创建LinkedHashMap的时候,指定了accessOrder为true的话,那么就会在访问LinkedHashMap的过程中,会对内部的元素重新排序,这就是实现LruCache的关键部分。
虽然我们设置了accessOrder为true实现了访问时的元素排序,但是还远远不够,因为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;
    // 同步代码块,使用 this 也就是说同时只能由一个线程操作这个对象
    synchronized (this) {
        putCount++;
        // 先通过safeSizeOf方法计算当前传入的 value 的大小,累加的 size
        size += safeSizeOf(key, value);
        // 把键值对插入到 LinkedHashMap 中,如果有返回值,说明存在相同的 key,取出旧值给 previous ,,这里很巧妙,没有先判断是否存在,而是默认直接插入,然后再根据返回值确定是否再-上面加的size
        previous = map.put(key, value);
        // 如果存在旧值,则从当前大小中删除旧值占用的大小.
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }
    // 如果 存在旧值,相当于把旧值移除了,这里调用 entryRemoved 方法.
    // entryRemoved 默认是空实现,如果用户有需求,可以自己实现,完成一些资源的释放工作.
    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }
    // 这个是最关键的方法,用来计算当前大小是否符合要求.
    trimToSize(maxSize);
    // 返回旧值
    return previous;
}

在 put 方法里面我们看到了,使用 LruCache 缓存是不允许键值对为空的,并且在执行插入操作的时候,使用了 Synchronized 关键字对代码进行线程同步,保证了插入操作的线程安全。
然后计算了当前插入值的大小,累加到 size 上,执行完插入操作以后,如果之前存在相同的 key 值,则把之前元素的大小从 size 上面给移除掉。如果没有存在,就什么也不做。
如果用户重写了 entryRemoved 操作,也会回调 entryRemoved方法,让用户执行一些资源释放等工作。
最后调用了trimToSize(maxSize) 方法,这个方法是个核心方法,主要计算当前大小是否超过了设置的最大值,超过了则会将最近最少使用的元素移除。
trimToSize() 控制缓存的容量
在 LruCache 里面,控制缓存容量不超过我们设置的最大值的关键点就是这个  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.entrySet() 代表从 LinkedHashMap 的头结点开始遍历,在
            // 上篇文章里面看了源码,可以参考下面的链接
            // 从头开始遍历,那只取一次,toEvict 就是头节点的元素
            Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
            // 要删除元素的 key
            key = toEvict.getKey();
            // 要删除元素的 value
            value = toEvict.getValue();
            // 使用 LinkedHashMap 的 remove 方法删除指定元素
            map.remove(key);
             // 重新计算当前 size 的大小
            size -= safeSizeOf(key, value);
            // 移除次数+1
            evictionCount++;
        }
        // 调用用户自定义的 entryRemoved()  如果用户定义了的话
        entryRemoved(true, key, value, null);
    }
}

首先开启了一个无限循环,在循环里面的同步代码块里面会判断当前的容量 size 是否超过最大容量 maxSize。如果没超过,结束循环。
如果超过,就会遍历内部的 LinkedHashMap 对象 map,这里使用的是 map.entrySet(),在上一篇LinkedHashMap 源码分析 里面我们对 LinkedHashMap 的遍历做了简单的介绍,map.entrySet() 最终是调用 LinkedHashIterator 里面的 nextNode 拿到节点,然后在 LinkedEntryIterator 里面从节点里面 通过 nextNode() 拿到 entry 的值。

上篇源码里面讲了,在 LinkedHashIterator 的构造方法里面是从头节点开始取值的,所以这里的调用的 next 方法拿的就是头节点。
所以在 trimToSize 方法里面主要做的事情就是:如果容量没超过最大值,返回,如果超过最大值,就依次移除头节点元素,一直到容量满足设定的最大值。

remove() 删除缓存
public final V remove(K key) {
    // 不允许 null 值
    if (key == null) {
        throw new NullPointerException("key == null");
    }
    // 删除的元素
    V previous;
    // 同步代码块保证线程安全
    synchronized (this) {
        // 删除元素,并把值赋给 previous
        previous = map.remove(key);
        //如果之前有 key 对应的值,将其减去
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }
    // 如果用户重写了entryRemoved 并且 之前有与 key 对应的值,执行entryRemoved。
    if (previous != null) {
        entryRemoved(false, key, previous, null);
    }
    return previous;
}

这里也很简单,住要是通过内部的 LinkedHashMap 移除元素,然后再把原来缓存中的对应的值删掉。

get()获取缓存
public final V get(K key) {
    // 不允许 null key
    if (key == null) {
        throw new NullPointerException("key == null");
    }
    // value 的值
    V mapValue;
    // 同步代码块保证当前实例的线程安全
    synchronized (this) {
        // 通过 LinkedHashMap 的 get 方法去寻找
        mapValue = map.get(key);
        // 找到只,直接返回,命中值 +1 
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        // 没找到,未命中次数+1
        missCount++;
    }
    // 这个地方意思,没有通过 get 方法找到,但是你想要有返回值,那么就可以重写 create 方法自己创建一个 返回值、。
    V createdValue = create(key);
    // 创建的值为 null ,直接返回 null
    if (createdValue == null) {
        return null;
    }
    synchronized (this) {
        createCount++;
        //将createdValue加入到map中,并且将原来键为key的对象保存到mapValue
        mapValue = map.put(key, createdValue);
        // 原来位置不为空,
        if (mapValue != null) {
            // There was a conflict so undo that last put
            // 撤销上一步的操作,依旧把原来的值放到缓存。,替换掉新创建的值
            map.put(key, mapValue);
        } else {
            // 原来key 对应的没值,计算当前缓存大小。
            size += safeSizeOf(key, createdValue);
        }
    }
    // 相当于一个替换操作,先用 createdValue 替换原来的值,然后这里移除掉 createdValue 。返回原来 key 对应的值。
    if (mapValue != null) {
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
    } else {
        // 调用trimToSize方法看是否需要回收旧数据
        trimToSize(maxSize);
        return createdValue;
    }
}

get方法前半部分是,从 map 里面取值,如果取到就返回。
如果没取到,并且重写了 create(K) 方法,就会先把 create(K) 方法创建的 value 保存到缓存,如果新创建的 value 保存的位置原来有值,就会替换回来。并且执行 entryRemoved 方法给调用者回调。
前面也讲了,LruCache 会根据元素的访问顺序进行排序。其实这里内部调用 LinkedHashMap 的 get 或者 put 方法的时候会调用到 afterNodeAccess 方法, 在 LinkedHashMap 的 afterNodeAccess 方法中对内部元素排序,这在上一篇 LinkedHashMap 中有讲到。

evictAll清除全部缓存数据
public final void evictAll() {
    trimToSize(-1); // -1 will evict 0-sized elements
}

总结

  1. LruCache 中维护了一个 LinkedHashMap,该 LinkedHashMap 创建的时候,设置了 accessOrder 为 true,其内部元素不是已插入顺序排序,而是以访问顺序排序的。
  2. 当调用put()方法获取数据的时候,会在内部的 map 中添加元素,并调用 trimToSize() 判断缓存是否已满,如果满了就删除 LinkedHashMap 中位于头节点的元素,即近期最少访问的元素。
  3. 当调用 get() 方法访问缓存对象时,就会调用 LinkedHashMap 的 get() 方法获得对应集合元素,进而调用 LinkedHashMap 内部实现的 afterNodeAccess 方法将元素移动到尾节点。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值