Android LruCache源码解析


参考:

LruCache 源码解析

LruCache源码解析

LruCache 注释源码

LruCache源码解析_效果展示

Android开发中,为了减少用户的流量使用和使APP体验更流畅,我们通常会使用缓存技术。通常来说,缓存分两级。

  • 第一级,是内存缓存,它的好处是,读写非常快,缺点则是,过量地使用会使APP整体变得十分卡顿,因为运行的内存不足了,甚至引起OOM。
  • 第二级,是文件缓存(File,SQLite等),文件缓存的读写效率要低于内存缓存。但是空间更加的充足。

一级缓存由于空间很有限,我们通常会为它设置一个size,当超过这个size时,缓存会将不常用的内容清掉。

Android中提供了一个方便的容器,用来处理这个缓存问题——LruCache

1. 简介

LRU 是 Least Recently Used 最近最少使用算法。

曾经,在各大缓存图片的框架没流行的时候。有一种很常用的内存缓存技术:SoftReference 和 WeakReference(软引用和弱引用)。但是走到了 Android 2.3(Level 9)时代,垃圾回收机制更倾向于回收 SoftReference 或 WeakReference 的对象。后来,又来到了 Android3.0,图片缓存在内容中,因为不知道要在是什么时候释放内存,没有策略,没用一种可以预见的场合去将其释放。这就造成了内存溢出。

2. 使用方法

  1. (必填)你需要提供一个缓存容量作为构造参数。
  2. (必填) 覆写 sizeOf 方法 ,自定义设计一条数据放进来的容量计算,如果不覆写就无法预知数据的容量,不能保证缓存容量限定在最大容量以内。
  3. (选填) 覆写 entryRemoved 方法 ,你可以知道最少使用的缓存被清除时的数据( evicted, key, oldValue, newVaule )。
  4. (记住)LruCache是线程安全的,在内部的 get、put、remove 包括 trimToSize 都是安全的(因为都上锁了)。

使用如下:

  	private static final float ONE_MIB = 1024 * 1024;
    // 7MB
    private static final int CACHE_SIZE = (int) (7 * ONE_MIB);
    private LruCache<String, Bitmap> bitmapCache;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.bitmapCache = new LruCache<String, Bitmap>(CACHE_SIZE) {
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount();
            }

            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
        
            }
        };

    }

3. 源码分析

LruCache 原理概要解析

LruCache 就是 利用 LinkedHashMap 的一个特性( accessOrder=true 基于访问顺序 )再加上对 LinkedHashMap 的数据操作上锁实现的缓存策略。

LruCache 的数据缓存是内存中的。

  1. 首先设置了内部 LinkedHashMap 构造参数 accessOrder=true, 实现了数据排序按照访问顺序。

  2. 然后在每次 LruCache.get(K key) 方法里都会调用 LinkedHashMap.get(Object key)。

  3. 如上述设置了 accessOrder=true 后,每次 LinkedHashMap.get(Object key) 都会进行 LinkedHashMap.afterNodeAccess(Node<K,V> e)。

  4. LinkedHashMap 是双向循环链表,然后每次 LruCache.get -> LinkedHashMap.get 的数据就被放到最末尾了。

  5. 在 put 和 trimToSize 的方法执行下,如果发生数据量移除,会优先移除掉最前面的数据(因为最新访问的数据在尾部)。

主要的成员变量

	//主要的存储成员变量
	private final LinkedHashMap<K, V> map;
  	/**
     * 缓存大小的单位。不规定元素的数量。
     */
    // 已经存储的数据大小
    private int size;
    // 最大存储大小
    private int maxSize;
    // 调用put的次数
    private int putCount;
    // 调用create的次数
    private int createCount;
    // 收回的次数 (如果出现)
    private int evictionCount;
    // 命中的次数(取出数据的成功次数)
    private int hitCount;
    // 丢失的次数(取出数据的丢失次数)
    private int missCount;

LruCache 的唯一构造方法

/**
 * LruCache的构造方法:需要传入最大缓存个数
 */
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
     /*
     * 初始化LinkedHashMap
     * 第一个参数:initialCapacity,初始大小
     * 第二个参数:loadFactor,负载因子=0.75f
     * 第三个参数:accessOrder=true,基于访问顺序;accessOrder=false,基于插入顺序
     */
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
  1. 第一个参数 initialCapacity 用于初始化该 LinkedHashMap 的大小。

  2. 第二个参数 loadFactor,这个其实的 HashMap 里的构造参数,涉及到扩容问题,比如 HashMap 的最大容量是100,那么这里设置0.75f的话,到75容量的时候就会扩容。

  3. 第三个参数 accessOrder=true ,这样的话 LinkedHashMap 数据排序就会基于数据的访问顺序,从而实现了 LruCache 核心工作原理。

LruCache.get(K key)

    /**
     * 根据key查询缓存,如果存在于缓存或者被create方法创建了。
     * 如果值返回了,那么它将被移动到双向循环链表的的尾部。
     * 如果如果没有缓存的值,则返回null。
     */
    public final V get(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {
            // LinkHashMap 如果设置按照访问顺序的话,这里每次get都会重整数据顺序
            mapValue = map.get(key);
            // 计算 命中次数
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            // 计算 丢失次数
            missCount++;
        }

        /*
         * 官方解释:
         * 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。如果在create()执行的时
         * 候,一个冲突的值被添加到Map,我们在Map中删除这个值,释放被创造的值。
         */
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }

        /***************************
         * 不覆写create方法走不到下面 *
         ***************************/

        /*
         * 正常情况走不到这里
         * 走到这里的话 说明 实现了自定义的 create(K key) 逻辑
         * 因为默认的 create(K key) 逻辑为null
         */
        synchronized (this) {
            // 记录 create 的次数
            createCount++;
            // 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值
            mapValue = map.put(key, createdValue);

            // 如果之前存在相同key的value,即有冲突。
            if (mapValue != null) {
                /*
                 * 有冲突
                 * 所以 撤销 刚才的 操作
                 * 将 之前相同key 的值 重新放回去
                 */
                map.put(key, mapValue);
            } else {
                // 拿到键值对,计算出在容量中的相对长度,然后加上
                size += safeSizeOf(key, createdValue);
            }
        }

        // 如果上面 判断出了 将要放入的值发生冲突
        if (mapValue != null) {
            /*
             * 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了
             * 告诉 自定义 的 entryRemoved 方法
             */
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            // 上面 进行了 size += 操作 所以这里要重整长度
            trimToSize(maxSize);
            return createdValue;
        }
    }

LinkedHashMap.get(Object key)

public V get(Object key) {
    Node<K,V> e;
    // 调用 HashMap  get 方法
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // 如果设置了 LRU 策略
    if (accessOrder)
    // 这个方法把当前 key 移动到队尾
        afterNodeAccess(e);
    return e.value;
}

从上述源码中,可以看到,通过 afterNodeAccess 方法把当前访问节点移动到了队尾,其实不仅仅是 get 方法,执行 getOrDefault、compute、computeIfAbsent、computeIfPresent、merge 方法时,也会这么做,通过不断的把经常访问的节点移动到队尾,那么靠近队头的节点,自然就是很少被访问的元素了。

afterNodeAccess方法如下:

	//把当前访问节点移动到了队尾
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMapEntry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMapEntry<K,V> p =
                (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

LinkedHashMap 是双向循环链表,然后此次 LruCache.get -> LinkedHashMap.get 的数据就被放到最末尾了。

以上就是 LruCache 核心工作原理。

双向循环链表图如下所示:
在这里插入图片描述


接下来介绍 LruCache 的容量溢出策略。

LruCache.put(K key, V value)

	/**
     * 给对应key缓存value,该value将被移动到队头。
     */
    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);
            /*
             * 放入 key value
             * 如果 之前存在key 则返回 之前key 的value
             * 记录在 previous
             */
            previous = map.put(key, value);
            // 如果存在冲突
            if (previous != null) {
                // 计算出 冲突键值 在容量中的相对长度,然后减去
                size -= safeSizeOf(key, previous);
            }
        }
         // 如果上面发生冲突
        if (previous != null) {
            /*
             * previous值被剔除了,此次添加的 value 已经作为key的 新值
             * 告诉 自定义 的 entryRemoved 方法
             */
            entryRemoved(false, key, previous, value);
        }
        trimToSize(maxSize);
        return previous;
    }

记住几点:

  1. put 开始的时候确实是把值放入 LinkedHashMap 了,不管超不超过你设定的缓存容量。
  2. 然后根据 safeSizeOf 方法计算 此次添加数据的容量是多少,并且加到 size 里 。
  3. 说到 safeSizeOf 就要讲到 sizeOf(K key, V value) 会计算出此次添加数据的大小 。
  4. 直到 put 要结束时,进行了 trimToSize 才判断 size 是否 大于 maxSize 然后进行最近很少访问数据的移除。

LruCache.trimToSize(int maxSize)

public void trimToSize(int maxSize) {
    /*
     * 这是一个死循环,
     * 1.只有 扩容 的情况下能立即跳出
     * 2.非扩容的情况下,map的数据会一个一个删除,直到map里没有值了,就会跳出
     */
    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!");
            }
            // 如果是 扩容 或者 map为空了,就会中断,因为扩容不会涉及到丢弃数据的情况
            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);
    }
}

简单描述:会判断之前 size 是否大于 maxSize 。是的话,直接跳出后什么也不做。不是的话,证明已经溢出容量了。由于最近经常访问的数据在最末尾。拿到一个存放 key 的 Set,然后一直一直从头开始删除,删一个判断是否溢出,直到没有溢出。

覆写 entryRemoved 的作用

/**
 * 1.当被回收或者删掉时调用。该方法当value被回收释放存储空间时被remove调用
 * 或者替换条目值时put调用,默认实现什么都没做。
 * 2.该方法没用同步调用,如果其他线程访问缓存时,该方法也会执行。
 * 3.evicted=true:如果该条目被删除空间 (表示 进行了trimToSize or remove)  evicted=false:put冲突后 或 get里成功create后
 * 导致
 * 4.newValue!=null,那么则被put()或get()调用。
 */
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
}

entryRemoved被LruCache调用的场景:

  1. (put) put 发生 key 冲突时被调用,evicted=false,key=此次 put 的 key,oldValue=被覆盖的冲突 value,newValue=此次 put 的 value。
  2. (trimToSize) trimToSize 的时候,只会被调用一次,就是最后一次被删除的最少访问数据带回来。evicted=true,key=最后一次被删除的 key,oldValue=最后一次被删除的 value,newValue=null(此次没有冲突,只是 remove)。
  3. (remove) remove的时候,存在对应 key,并且被成功删除后被调用。evicted=false,key=此次 put的 key,oldValue=此次删除的 value,newValue=null(此次没有冲突,只是 remove)。
  4. (get后半段,查询丢失后处理情景,不过建议忽略) get 的时候,正常的话不实现自定义 create 的话,代码上看 get 方法只会走一半,如果你实现了自定义的 create(K key) 方法,并且在 你 create 后的值放入 LruCache 中发生 key 冲突时被调用,evicted=false,key=此次 get 的 key,oldValue=被你自定义 create(key)后的 value,newValue=原本存在 map 里的 key-value。

4. 总结

LruCache重要的几点:

  1. LruCache 是通过 LinkedHashMap 构造方法的第三个参数的 accessOrder=true 实现了 LinkedHashMap 的数据排序基于访问顺序 (最近访问的数据会在链表尾部),在容量溢出的时候,将链表头部的数据移除。从而,实现了 LRU 数据缓存机制。

  2. LruCache 在内部的get、put、remove包括 trimToSize 都是安全的(因为都上锁了)。

  3. LruCache 自身并没有释放内存,将 LinkedHashMap 的数据移除了,如果数据还在别的地方被引用了,还是有泄漏问题,还需要手动释放内存。

  4. 覆写 entryRemoved 方法能知道 LruCache 数据移除是是否发生了冲突,也可以去手动释放资源。

  5. maxSize 和 sizeOf(K key, V value) 方法的覆写息息相关,必须相同单位。( 比如 maxSize 是7MB,自定义的 sizeOf 计算每个数据大小的时候必须能算出与MB之间有联系的单位 )

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值