用烂LruCache,隔壁产品看不懂了

1,660 阅读1分钟

1、它是什么?

它是数据存储容器,可用于做数据缓存载体,优先淘汰那些近期最少使用的数据。LRU的全称为Least Recently Used。

2、如何使用?

2.1 简单使用

以kotlin代码为例,简洁使用如下

var lruCache: LruCache<Int, String>? = null
val maxCacheSize = 8


fun main() {
    lruCache = LruCache(maxCacheSize)
    for (index in 0..10) {
        lruCache?.put(index, "$index")
    }
    print("\n===end===")
}

输出如下,可以看到,最早加入LruCache的数据被淘汰了

2.2 覆写sizeOf方法

上述例子中,存储对象的大小默认为1,实际使用场景中,往往需要传入内存大小,这时候需要覆写 sizeOf 方法,告知LruCache每个存储对象的大小。

var imgCache: LruCache<Int, Bitmap>? = null
var maxImgCacheSize: Int = 3072

@Test
fun test() {
    imgCache = object : LruCache<Int, Bitmap>(maxImgCacheSize) {
        override fun sizeOf(key: Int, value: Bitmap): Int {
            if (value == null) {
                return 0
            }
            return value.width * value.height
        }
    }
    var tempBm50 = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888)
    imgCache?.put(0, tempBm50)
    var tempBm40 = Bitmap.createBitmap(40, 40, Bitmap.Config.ARGB_8888)
    imgCache?.put(0, tempBm40)

    print("\n===end===")
}

输出如下,可以看到,加入宽高为40的bitmap时,宽高为50的bitmap被淘汰了

3、谁用了它?而它又用了谁?

3.1 谁用了它?

Glide中的内存缓存就是通过LruCache实现的,具体的实现类为LruResourceCache。类似缓存策略的还有DiskLruCache。

3.2 它用了谁?

LruCache的代码量不算多,内部持有LinkedHashMap。

4、看看原理

看类结构不难发现,LruCache直接继承自Object,核心属性为LinkedHashMap类型的map。

4.1 LinkedHashMap

继承自HashMap,实现了Map接口

4.1.1 HashMap结构图

HashMap为数组和链表组合的数据结构,在jdk1.8版本后,当链表长度大于8时,转换为红黑树存储。优点是执行效率高,如查询、插入等操作。

4.1.2 LinkedHashMap结构图

LinkedHashMap继承自HashMap,可以直观地看到,基本结构还是数组与单向链表地结合,区别在于所有元素还通过双向链表关联,值得注意的是内部持有属性head、tail,分别表示双向链表表头、表尾,方便检索。

4.1.3 添加元素

调用put方法,即父类HashMap的 put 方法,即调用 putVal 方法,其中有个关键方法 afterNodeAccess ,LinkedHashMap重写了这个方法

afterNodeAccess 方法,这个方法的作用是将结点移动至双向链表尾部,属性tail被重新赋值,当LruCache检索最近使用的元素时可以方便获取。

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

4.1.4 获取元素

调用 get 方法,即调用父类HashMap的 get 方法,可以看到当 accessOrder 为 true 时,仍然会调用 afterNodeAccess 方法,accessOrder 是何时被设置的呢?

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

在LruCache的构造方法中,accessOrder 被置为了 true。

4.1.5 删除元素

调用 remove 方法,即HashMap的 remove 方法,即 removeNode 方法,内部调用LinkedHashMap重写的 afterNodeRemoval 方法。

afterNodeRemoval 方法,此方法比较简单,解除双向链表的关联,同时移动head或tail的指向。

void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMapEntry<K,V> p =
        (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

4.1.6 扩容

即HashMap的 resize 方法,当超过容器设定的阈值时,一般扩容至原来的两倍。

4.2 LruCache的添加元素

调用 put 方法,内部除了调用 LinkedHashMap的put方法外,还调用了sizeOf方法,用于计算当前元素的大小。不覆写 sizeOf 方法的话,元素大小默认记为 1。

protected int sizeOf(@NonNull K key, @NonNull V value) {
    return 1;
}

最后调用 trimToSize 方法,判断是否需要淘汰最少使用的元素并执行淘汰算法。当有元素被淘汰时,回调 entryRemoved 方法供调用者感知,需覆写该方法。

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

4.3 LruCache获取元素

一般情况下还是会调用LinkedHashMap的 get 方法,值得注意的是,当在LinkedHashMap找不到key对应的value时,LruCache会尝试自造一个null元素结点与key对应,,最后将元素结点加入到LinkedHashMap中。

public final V get(@NonNull K key) {
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }

    /*
     * Attempt to create a value. This may take a long time, and the map
     * may be different when create() returns. If a conflicting value was
     * added to the map while create() was working, we leave that value in
     * the map and release the created value.
     */

    V createdValue = create(key);
    if (createdValue == null) {
        return 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;
    }
}

4.4 LruCache删除元素

调用 remove 方法,同样的,回调 entryRemoved 方法可供调用者感知元素的淘汰情况。

public final V remove(@NonNull 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;
}

5、适用场景

适合需要LRU缓存策略的场景,如建立View缓冲池、图片资源缓冲池等等。

图中的调用者可以是ImageView、RecyclerView、Fragment等等,有了LruCache对源数据进行缓存过滤,保证调用者能立即使用到最常用的数据,提升程序效率。

6、注意事项

6.1 执行java的main方法

本文的代码示例使用androidTest进行的测试,如果要在java代码中直接进行测试,需要做特殊配置。如果是java文件,需要注意AndroidStudio中运行java文件的main方法时,需要配置 gradle.xml 文件,额外注意 <option name="delegatedBuild" value="false" /> 配置。当然,也可用通过Tools->Groovy Console运行。

<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="GradleSettings">
    <option name="linkedExternalProjectsSettings">
      <GradleProjectSettings>
        <option name="delegatedBuild" value="false" />
        <option name="distributionType" value="DEFAULT_WRAPPED" />
        <option name="externalProjectPath" value="$PROJECT_DIR$" />
        <option name="modules">
          <set>
            <option value="$PROJECT_DIR$" />
            <option value="$PROJECT_DIR$/app" />
          </set>
        </option>
        <option name="resolveModulePerSourceSet" value="false" />
        <option name="testRunner" value="PLATFORM" />
      </GradleProjectSettings>
    </option>
  </component>
</project>

6.2 多线程场景

LruCache中考虑了多线程的场景,如果单独使用LinkedHashMap,需要注意多线程操作的场景,HashMap在多线程中可能存在的问题LinkedHashMap中同样存在。