Android LruCache 源码分析

Posted 吴豪杰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android LruCache 源码分析相关的知识,希望对你有一定的参考价值。

0. 前言

学过操作系统这门课的朋友都还记得 LRU 这个算法吧,中文名叫”最近最久未使用”,它是用在页面置换策略中的一种很巧妙的淘汰算法,而在 android 中,也有一个缓存淘汰机制用到了它,叫做 LruCache,它也可以说是一个精妙的设计吧,这篇博文中,笔者将带领大家剖析它源码中的精妙之处…

1. 初始化

LruCache 类源码位于 android.util.LruCache 包下,大家也可以同步阅读。第一件事,便是看其实例化过程,只有一个带参数的构造函数,参数的意思是缓存最大支持的内存容量,注意哦,不是数量,是占用空间的容量:

public LruCache(int maxSize) 
    if (maxSize <= 0) 
        throw new IllegalArgumentException("maxSize <= 0");
    
    // 赋值
    this.maxSize = maxSize;
    // 创建 HashMap
    // 参数:初始容量为0;加载因子为0.75;以最近最久未使用排序
    this.map = new LinkedHashMap<K, V>(0, 0.75f, true);

它将最大容量赋给了成员变量,还创建了一个 LinkedHashMap,它是一个循环链表,前面两个参数不要紧,关键在最后一个参数,最后一个参数含义是”是否以最近访问的顺序排序”,也就是说,如果为 true,那么 HashMap 的链表将会以最近访问的元素在尾部,很久没访问的元素在头部的顺序来排序,在 LinkedHashMap 的源码中可以证实这一说法:

// 调用此函数说明产生一次访问记录
public V get(Object key) 
    ...
    // 遍历链表
    for (...; e != null; e = e.next) 
        K eKey = e.key;
        // 如果找到值
        if (eKey == key || ...) 
            if (accessOrder)
                // 将结点e移至循环链表尾部
                makeTail((LinkedEntry<K, V>) e);
            return e.value;
        
    
    return null;


private void makeTail(LinkedEntry<K, V> e) 
    e.prv.nxt = e.nxt;
    e.nxt.prv = e.prv;
    // 将 e 去除
    // --------------------
    // |                  ↓
    // pre      e        nxt
    //  ↑                 |
    //  -------------------

    LinkedEntry<K, V> header = this.header;
    LinkedEntry<K, V> oldTail = header.prv;
    e.nxt = header;
    e.prv = oldTail;
    oldTail.nxt = header.prv = e;
    // 将 e 加在循环链表尾部: 
    // Tail(header的前面一个元素) 和 Header 中间
    // -------------- --------------
    // |            ↓ ↓            |
    // tail          e           header
    // ↑            | |            ↑
    // -------------- --------------
    modCount++;

并且,通过 LinkedHashMap#eldest() 方法可以返回最老的结点:

public Entry<K, V> eldest() 
    // 头第一个结点便是最老的节点
    LinkedEntry<K, V> eldest = header.nxt;
    // 如果 header 的下一个结点就是 header 
    // 说明该循环链表为空
    return eldest != header ? eldest : null;

2. 添加

初始化步骤为我们提供了一个可供存储缓存的循环链表,还提供好了 LRU 排序。接下来看看如何添加一个缓存记录的:

public final V put(K key, V value) 
    if (key == null || value == null) 
        throw new NullPointerException("key == null || value == null");
    
    V previous;
    synchronized (this) 
        putCount++; // 放入的次数 调用 remove() 不会减少
        size += safeSizeOf(key, value); // 占用的大小 默认为1 供子类重写 sizeOf()
        previous = map.put(key, value); // 放入 LinkedHashMap 返回值表示先前是否存在有记录
        if (previous != null)  // 如果之前缓存中存在有记录,那么并没有放入
            size -= safeSizeOf(key, previous);  // 所以大小要减少
        
    
    if (previous != null) 
        entryRemoved(false, key, previous, value);  // 调用供子类覆写的方法
    
    trimToSize(maxSize);    // 重建大小 淘汰旧的元素
    return previous;

不少的代码,其实就是两步,先将缓存结点添加进 LinkedHashMap,然后调用 trimToSize(maxSize) 检查大小并淘汰。接下来看看是如何淘汰就节点的:

public void trimToSize(int maxSize) 
    while (true) 
        K key;
        V value;
        synchronized (this) 
            ...
            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++; // 移除的次数
        
        // 子类重写方法以实现具体的移除策略
        entryRemoved(true, key, value, null);
    

上面两段代码都提到了 entryRemoved() 方法,这其实是一个供子类扩展功能的方法,它可以被子类覆写,比如可以再增加一级磁盘缓存,那么磁盘上的添加和移除缓存方法就需要子类来重写了:

// 子类重写方法 以实现具体的移除策略
// 参数一 表示 是否是被淘汰出去的
// 参数三 表示 移除了一个旧的值 用新的值来替换
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) 

// 子类重写方法 以实现具体的添加策略
// 参数一 表示 键值对的键
protected V create(K key) 
    return null;

3. 取出

添加好了缓存就可以取出来用了,接下来看是如何取出来的:

public final V get(K key) 
    ...
    V mapValue;
    synchronized (this) 
        mapValue = map.get(key);
        if (mapValue != null) 
            hitCount++; // 用到的元素数目 越大相当于利用率越高
            return mapValue; // 找到了就直接返回
        
        missCount++;    // 没有找到元素 丢失的缓存数目
    
    // 如果缓存容器中没有值 就调用 create() 方法再创建
    V createdValue = create(key);
    if (createdValue == null) 
        return null;    // 创建不成功 返回 null
    
    synchronized (this) 
        createCount++; // 创建次数
        mapValue = map.put(key, createdValue);  // 放入缓存 特别注意 返回值是表示之前是否存在有相同值
        if (mapValue != null) 
            // There was a conflict so undo that last put
            map.put(key, mapValue); // 如果不为空 说明之前就存在 那么再次放之前的引用就可以了
         else 
            size += safeSizeOf(key, createdValue);  // 放入成功的话就增加大小空间
        
    
    if (mapValue != null) 
        entryRemoved(false, key, createdValue, mapValue);   // 调用
        return mapValue;
     else 
        trimToSize(maxSize);    // 如果放进去了 就得再次淘汰
        return createdValue;
    

其实完成的内容也很简单,就是从 LinkedHashMap 中取出,如果有就返回,如果没有就调用子类的 create(key) 方法,最后要记得验证是否需要淘汰最近最久未使用的元素。

总结

LruCache 的源码还是很简单的,但它很巧妙地结合 LinkedHashMap 实现了 LRU 算法,可以说非常值得一读!

以上是关于Android LruCache 源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Java源码分析Android-LruCache源码分析

Java源码分析Android-LruCache源码分析

LruCache源码浅析

Android源码解析——LruCache

LruCache 源码分析

Picasso源码解析之Lrucache算法源码解析