LruCache

Android知识总结

一、简介

LruCache(Least Recently Used)算法的核心思想就是最近最少使用算法。他在算法的内部维护了一个LinkHashMap的链表,LinkedHashMap 是由数组+双向链表的数据结构来实现的,通过put数据的时候判断是否内存已经满了,如果满了,则将最近最少使用的数据给剔除掉,从而达到内存不会爆满的状态。

通过上面这张图,我们可以看到,LruCache算法内部其实是一个队列的形式在存储数据,先进来的数据放在队列的尾部,后进来的数据放在队列头部,如果要使用队列中的数据,那么使得之后将其又放在队列的头部,如果要存储数据并且发现数据已经满了,那么便将队列尾部的数据给剔除掉,从而达到我们使用缓存的目的。这里需要注意一点,队尾存储的数据就是我们最近最少使用的数据,也就是我们在内存满的时候需要剔除掉的数据。

二、代码分析

1、构造方法和参数

public class LruCache<K, V> {
    //定义一个LinkedHashMap,有序的 map
    private final LinkedHashMap<K, V> map;

    private int size;//初始大小
    private int maxSize;//最大容量

    private int putCount;//插入个数
    private int createCount;//创建个数
    private int evictionCount;//回收个数
    private int hitCount;//找到key的个数
    private int missCount;//没找到key的个数

    //构造函数,传递进来一个最大容量值
    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);
    }

    //设置cache的大小
    public void resize(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }

        synchronized (this) {
            this.maxSize = maxSize;
        }
        trimToSize(maxSize);
    }
}

2、 get 方法分析

     //如果通过key查找到value存在于cache中就直接返回或者通过create方法创建一个然后返回
     //如果这个值被返回了,那么它将移动到队列的头部
     //如果一个值没有被缓存同时也不能被创建则返回null
    public final V get(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {
            mapValue = map.get(key); // 获取 Value
            //找到对应值,命中+1,直接返回该值
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            //否则未命中+1
            missCount++;
        }

        //如果没找到key对应的值那么就尝试创建一个,也许花费较长的时间
        //并且创建后返回的map也许和之前的不同,如果创建的值和map中有冲突的话
        //那么我们就释放掉创建的值,保留map中的值。

        V createdValue = create(key);
        //通过观察后面的create()方法,可以看到直接return null;
        //那么我们需要想一想为什么源码中是直接返回null呢?
        //因为LruCache常常作为内存缓存而存在,所以当我们查找key找不到对应的value时
        //这个时候我们应该从其他方面,比如文件缓存或者网络中请求数据
        //而不是我们随便赋值创建一个值返回,所以这里返回null是合理的。
        //如果自己真的有需要的话,自己需要重写create方法,手动创建一个值返回
        if (createdValue == null) {
            return null;
        }

        //走到这儿说明创建了一个不为null的值
        synchronized (this) {
            createCount++;//创建个数+1
            //把创建的value插入到map对应的key中
            //并且将原来键为key的对象保存到mapValue
            mapValue = map.put(key, createdValue);

            if (mapValue != null) {
                //如果mapValue不为空,说明原来key对应的是有值的,则撤销上一步的put操作。
                map.put(key, mapValue);
            } else {
                //加入新创建的对象之后需要重新计算size大小
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            //每次新加入对象都需要调用trimToSize方法看是否需要回收
            trimToSize(maxSize);
            return createdValue;
        }
    }
  • LinkedHashMap 的 get 方法
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;
}

get() 方法其实最关键就是 afterNodeAccess(),现在重点分析:

// 这个方法的作用就是将刚访问过的元素放到集合的最后一位
void afterNodeAccess(Node < K, V > e) {
    LinkedHashMap.Entry < K, V > last;
    if (accessOrder && (last = tail) != e) {
        // 将 e 转换成 LinkedHashMap.Entry
        // b 就是这个节点之前的节点
        // a 就是这个节点之后的节点
        LinkedHashMap.Entry < K, V > p = (LinkedHashMap.Entry < K, V > ) e, b = p.before, a = p.after;
        
        // 将这个节点之后的节点置为 null
        p.after = null;
        
        // b 为 null,则代表这个节点是第一个节点,将它后面的节点置为第一个节点
        if (b == null) head = a;
        
        // 如果不是,则将 a 上前移动一位
        else b.after = a;
        // 如果 a 不为 null,则将 a 节点的元素变为 b
        if (a != null) a.before = b;
        else last = b;
        if (last == null) head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

3、put 方法分析

现在以注释的方式来解释该方法的原理。

    //将key对应的value缓存起来,放在队列的头部
    //返回key对应的之前的旧值
    public final V put(K key, V value) {
        // 如果 key 或者 value 为 null,则抛出异常
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        synchronized (this) {
            // 加入元素的数量,在 putCount() 用到
            putCount++;//插入数量+1
            // 回调用 sizeOf(K key, V value) 方法,这个方法用户自己实现,默认返回 1
            size += safeSizeOf(key, value);
            //得到key对应的前一个value,如果之前无值,返回null,如果有值,返回前一个值
            previous = map.put(key, value);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            // 该方法默认方法体为空
            entryRemoved(false, key, previous, value);
        }

        trimToSize(maxSize);
        //返回之前key对应的旧值value
        return previous;
    }

    //根据maxSize来调整内存cache的大小,如果maxSize传入-1,则清空缓存中的所有对象
    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!");
                }

                //直到缓存大小 size 小于或等于最大缓存大小 maxSize,则停止循环
                if (size <= maxSize) {
                    break;
                }
                // 取出 map 中第一个元素
                Map.Entry<K, V> toEvict = map.eldest();
                if (toEvict == null) {
                    break;
                }

                key = toEvict.getKey();
                value = toEvict.getValue();
                // 删除该元素
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;//回收个数+1
            }

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

put() 方法其实重点就在于 trimToSize() 方法里面,这个方法的作用就是判断加入元素后是否超过最大缓存数,如果超过就清除掉最少使用的元素。

4、remove 方法

    //从内存缓存中根据key值移除某个对象并返回该对象
    public final V remove(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V previous;
        synchronized (this) {
            previous = map.remove(key);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

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

        return previous;
    }

三、总结

  • 为什么LruCache内部原理的实现需要用到LinkHashMap来存储数据呐?

因为LinkHashMap内部是一个数组加双向链表的形式来存储数据,他能够保证插入时候的数据和取出来的时候数据的顺序的一致性。也就是说,我们以什么样的顺序插入数据,就会以什么样的顺序取出数据。并且更重要的一点是,当我们通过get方法获取数据的时候,这个获取的数据会从队列中跑到队列头来,从而很好的满足我们LruCache的算法设计思想。

  • LruCache 其实使用了 LinkedHashMap 维护了强引用对象?

总缓存的大小一般是可用内存的 1/8,当超过总缓存大小会删除最少使用的元素,也就是内部 LinkedHashMap 的头部元素。
当使用 get() 访问元素后,会将该元素移动到 LinkedHashMap 的尾部

推荐阅读更多精彩内容