AndroidX之LruCache库

2,084 阅读7分钟

参考文章:

LruCache  |  Android Developers

Android使用LruCache缓存 - 火神漫步 - 博客园

安卓LruCache_xxdw1992的博客-CSDN博客

简单的LRU队列kotlin实现_long for us-CSDN博客_kotlin 队列

kotlin编写一个简单的LRU缓存结构

LRU缓存算法的实现_iKun-CSDN博客_lru算法

超详细LinkedHashMap解析_求offer的菜鸡的博客-CSDN博客_linkedhashmap

一、简介

安卓系统对内存的消耗的条件是相当苛刻的,肆意使用内存会导致系统OOM并杀死程序,所以对于每个APP在使用内存时就要谨慎再谨慎。

LRU是近期最少使用的算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LrhCache和DisLruCache,分别用于实现内存缓存和硬盘缓存,其核心思想都是LRU缓存算法。

从android3.1开始,LruCache已经作为android源码的一部分维护在android系统中,为了兼容以前的版本,android的support-v4包也提供了LruCache的维护,如果App需要兼容到android3.1之前的版本就需要使用support-v4包中的LruCache,如果不需要兼容到android3.1则直接使用android源码中的LruCache即可,但DiskLruCache并不是android源码的一部分。

二、手写LRU

在使用LruCache库之前,不妨先思考如何手动实现LRU算法,这样可以更好的理解LruCache的实现。而实现LRU,就不得不提到LinkedHashMap。

2.1 LinkedHashMap

LinkedHashMap继承自HashMap,它的多种操作都是建立在HashMap操作的基础上的。同HashMap不同的是,LinkedHashMap维护了一个Entry的双向链表,保证了插入的Entry中的顺序。这也是Linked的含义。结构图如下:

image.png

插入顺序为key1,key2,key3,key4,那么就会维护一个红线所示的双向链表。

可以说,LinkedHashMap=HashMap+双向链表

利用LinkedHashMap的特点,既可以很方便的实现LRU算法,又可以高效的访问元素,即每次命中元素,都将该元素置于链表尾部(先删除,再添加),这样就能保证头部元素是最久未使用的元素,一旦缓存空间占满,则可替换掉头部元素,而LinkedHashMap也为我们提供了标志位accessOrderremoveEldestEntry(eldest)方法来实现LRU算法。

  • 如果accessOrder为true的话,则会把访问过的元素放在链表后面,放置顺序是访问的顺序
  • removeEldestEntry(eldest)供用户覆写,如果返回true,则删除最久未使用的元素

2.2 get操作源码

/**
* 调用hashmap的getNode方法获取到值之后,维护链表
* @param key
* @return
*/

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

afterNodeAccess会将新插入的节点放到尾部,同时这个方法受到accessOrder的控制。

//在节点被访问后根据accessOrder判断是否需要调整链表顺序
void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    //如果accessOrder为false,什么都不做
    if (**accessOrder** && (last = tail) != e) {
    //p指向待删除元素,b执行前驱,a执行后驱
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<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;
        //这里执行将p放到尾部
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        //保证并发读安全。
        ++modCount;
    }
}

2.3 addEntry源码

void addEntry(int hash, K key, V value, int bucketIndex) {
    //创建新的Entry,并插入到LinkedHashMap中
    createEntry(hash, key, value, bucketIndex); // 重写了HashMap中的createEntry方法
    //双向链表的第一个有效节点(header后的那个节点)为最近最少使用的节点,这是用来支持LRU算法的
    Entry<K,V> eldest = header.after;
    //如果有必要,则删除掉该近期最少使用的节点,
    //这要看对removeEldestEntry的覆写,由于默认为false,因此默认是不做任何处理的。
    if (**removeEldestEntry(eldest)** ) {
        removeEntryForKey(eldest.key);
    } else {
    //扩容到原来的2倍
        if (size >= threshold)
            resize(2 * table.length);
    }
}

这里通过removeEldestEntry(eldest)控制是否删除近期最少使用的节点。

2.4 使用LinkedHashMap实现LRU

class LRUCache extends LinkedHashMap {
    private int capacity;
    public LRUCache(int capacity) {
        //accessOrder为true
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

    public int get(int key) {
        return (int)super.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        super.put(key, value);
    }

    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > capacity;
    }
}

三、LruCache库

3.1 介绍

LruCache库内部也是使用LinkedHashMap对数据进行管理:

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; // 当前大小
    private int maxSize; // 最大容量
    private int putCount; // put次数
    private int createCount; // 创建次数
    private int evictionCount; // 回收次数
    private int hitCount; // 命中次数
    private int missCount; // 未命中次数
    
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        //accessOrder为true,即按访问排序
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
}
  • 必须覆写的方法:
    • protected int sizeOf(K key, V value):计算每个对象占用的空间大小,供LruCache控制缓存大小

在重写了 sizeOf 方法的情况下,maxSize 代表的就是我们每个元素大小之后累加的允许的最大值。

  • 可以覆写的方法
    • protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) :删除元素时的额外操作,比如释放资源
    • protected V create(K key):创建元素

3.2 关键代码分析

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

方法的最后调用了trimToSize(maxSize) 方法,这个方法是个核心方法,主要计算当前大小是否超过了设置的最大值,超过了则会将最近最少使用的元素移除。

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

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

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

3.3.5 evictAll() 清除全部缓存数据

public final void evictAll() {
    trimToSize(-1); // -1 will evict 0-sized elements
}

当执行到map.isEmpty()时,循环才会停止

if (size <= maxSize || map.isEmpty()) {
    break;
}

3.3 总结

  • LruCache,内部使用Map保存内存级别的缓存(可手动操作映射到磁盘缓存)
  • LruCache使用泛型可以设配各种类型
  • LruCache使用了Lru算法保存数据
  • LruCache只用使用put和get方法压入数据和取出数据