Android LruCache 缓存机制实现原理

关于Android的三级缓存,其中主要的就是内存缓存和硬盘缓存。这两种缓存机制的实现都应用到了LruCache算法,今天我们就从使用到源码解析,来彻底理解Android中的缓存机制。

一、Android中的缓存策略

一般来说,缓存策略主要包含缓存的添加、获取和删除这三类操作。如何添加和获取缓存这个比较好理解,那么为什么还要删除缓存呢?这是因为不管是内存缓存还是硬盘缓存,它们的缓存大小都是有限的。当缓存满了之后,再想其添加缓存,这个时候就需要删除一些旧的缓存并添加新的缓存。

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

二、LruCache的使用

LruCache是Android 3.1所提供的一个缓存类,所以在Android中可以直接使用LruCache实现内存缓存。而DisLruCache目前在Android 还不是Android SDK的一部分,但Android官方文档推荐使用该算法来实现硬盘缓存。

1.LruCache的介绍

LruCache是个泛型类,主要算法原理是把最近使用的对象用强引用(即我们平常使用的对象引用方式)存储在 LinkedHashMap 中。当缓存满时,把最近最少使用的对象从内存中移除,并提供了get和put方法来完成缓存的获取和添加操作。

2.LruCache的使用

LruCache的使用非常简单,我们就已图片缓存为例。

 

 int maxMemory = (int) (Runtime.getRuntime().totalMemory()/1024);
        int cacheSize = maxMemory/8;
        mMemoryCache = new LruCache<String,Bitmap>(cacheSize){
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes()*value.getHeight()/1024;
            }
        };

①设置LruCache缓存的大小,一般为当前进程可用容量的1/8。
②重写sizeOf方法,计算出要缓存的每张图片的大小。

注意:缓存的总容量和每个缓存对象的大小所用单位要一致。

三、LruCache的实现原理

LruCache的核心思想很好理解,就是要维护一个缓存对象列表,其中对象列表的排列方式是按照访问顺序实现的,即一直没访问的对象,将放在队尾,即将被淘汰。而最近访问的对象将放在队头,最后被淘汰。

如下图所示:

 

 

那么这个队列到底是由谁来维护的,前面已经介绍了是由LinkedHashMap来维护。
链接:https://www.jianshu.com/p/b49a111147ee

 

通过使用 LruCache, 查看 LinkedHashMap 源码, 分析 LRU 算法的具体实现细节.

先来一张分析图

LRU 算法描述

当序列达到设置的内存上限时, 丢弃序列中最近最少使用的元素.

LruCache

Android SDK 提供的使用了(Least Recently Used)最近最少使用算法的缓存类.

编写一个 LruCache, 用于缓存 Integer.

 

public class IntegerCache extends LruCache<String, Integer> {
    public IntegerCache(int maxSize) {
        super(maxSize);
    }

    @Override
    protected int sizeOf(String key, Integer value) {
        return Integer.SIZE;
    }
}

 

// 最大容量为 4 个 Integer
IntegerCache ca = new IntegerCache(4 * Integer.SIZE)
ca.put("1", 1);
ca.put("2", 2);
ca.put("3", 3);
ca.put("4", 4);
ca.get("4");
ca.put("5", 5);
ca.put("4", 4);
ca.put("6", 6);

缓存中内容:

 

{1=1}                // put 1
{1=1, 2=2}           // put 2
{1=1, 2=2, 3=3}      // put 3
{1=1, 2=2, 3=3, 4=4} // put 4
---
{1=1, 2=2, 3=3, 4=4} // get 4
{2=2, 3=3, 4=4, 5=5} // put 5
{2=2, 3=3, 5=5, 4=4} // put 4
{3=3, 5=5, 4=4, 6=6} // put 6

可见, 每次的 getput 操作, 都会造成序列中的重排序, 最近使用的元素在末尾, 最近最少使用的元素在头部, 当容量超过限制时会移出最近最少使用的元素.

LruCache 的构造

 

public class LruCache<K, V> {
    // 构造时就初始化的一个 LinkedHashMap
    private final LinkedHashMap<K, V> map;

    private int size;          /* 记录当前缓存占用的内存大小 */
    private int maxSize;       /* 最多能缓存的内存大小 */

    private int putCount;      /* 记录 put 调用的次数 */
    private int createCount;   /* 记录 create 调用的次数 */
    private int evictionCount; /* 记录被丢弃的对象个数 */
    private int hitCount;      /* 记录调用 get 时,缓存命中的次数 */
    private int missCount;     /* 记录调用 get 时,缓存未命中的次数 */

    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        // 初始容量为0, 扩容系数为 0.75, 排序模式: true 表示按访问排序, false 表示按插入排序, SDK 实现里固定为 ture
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }

LruCache 插入元素

 

public final V put(K key, V value) {
    V previous;
    synchronized (this) {
        putCount++;
        // 内存占用记录增加
        size += safeSizeOf(key, value);
        // 存入新的值, 并获取 key 对应的旧值
        previous = map.put(key, value);
        if (previous != null) {
            // 如果旧值存在, 就减去对应内存
            size -= safeSizeOf(key, previous);
        }
    }

    // 如果 size > maxSize, 就执行丢弃元素, 裁剪内存操作
    trimToSize(maxSize);
    return previous;
}

LurCache 获取缓存

 

public final V get(K key) {
    V mapValue;
    synchronized (this) {
        // 从缓存中获取 key 对应的 value, 如果存在就直接返回
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }

    // 如果缓存中没有, 就尝试创建一个对应对象, 该方法由子类实现, 可以返回 null
    V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }

    // 如果子类 create 返回了非 null 对象, 就把这个对象返回, 并插入到缓存中
    synchronized (this) {
        createCount++;
        mapValue = map.put(key, createdValue);
        // 上面 get 时得到了 null 才会走到这, 怎么在插入时旧值又跑出来了 ?
        if (mapValue != null) {
            // 这里应该是避免多线程访问时, 在 get 获取为 null 之后, 其他线程插入了对应的值, 所以这里把其他线程插入的值还原回去
            map.put(key, mapValue);
        } else {
            // 如果没有其他插入, 就把新创建的内存占用记账
            size += safeSizeOf(key, createdValue);
        }
    }
    ...
}

以上就是 LruCache 里主要的方法了, 看完也没发现与 LRU 算法有关的东西, 那 LRU 的具体实现肯定就在 LinkedHashMap 里了.

LinkedHashMap 的实现

  • 内部数据结构: 双向链表

 

// LinkedHashMap 的节点数据结构, 继承自 HashMap.Node
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
    LinkedHashMapEntry<K,V> before, after;
    LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

构造

 

public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    // accessOrder 决定内部的排序顺序
    this.accessOrder = accessOrder;
}

获取操作

 

public V get(Object key) {
    Node<K,V> e;
    // 调用父类 HashMap 的方法
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // 如果按访问顺序排序为 ture, 则进行重排序
    if (accessOrder)
        // 将 e 移动到最后
        afterNodeAccess(e);
    return e.value;
}

可以看到, 重点就是 afterNodeAccess 这个方法.

访问 node 之后的排序操作

 

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, /* p 指向当前节点 e */
        b = p.before,   /* b 指向前一个节点 */
        a = p.after;    /* a 指向后一个节点 */
        p.after = null; /* 当前节点 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;
    }
}

Case 1: 访问元素的前后存在元素

初始状态移动指向最终结果

image

image

image

Case 1.2: 访问元素的前后存在多个元素

初始状态移动指向最终结果

image

image

image

Case 2: 访问元素的后面存在元素, 前面不存在

初始状态移动指向最终结果

image

image

image

Case 3: 访问元素的前面存在元素, 后面不存在

这种 case 不会做排序操作, 因为元素已经位于链表尾部了.


在访问元素之后, 通过 afterNodeAccess 排序之后, 被访问的元素就移动到了链表的尾部.

插入操作

LinkedHashMap 的 put 操作是直接调用父类 HashMap 的, HashMap 的 put 操作之后, 被插入的元素将会位于链表的尾部, 然后会调用 afterNodeInsertion, 该方法在 LinkedHashMap 中的实现:

 

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMapEntry<K,V> first;
    // 如果 removeEldestEntry 为 true, 则移出头部的元素
    // LinkedHashMap 中 removeEldestEntry 默认返回 false
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

由于 LinkedHashMapremoveEldestEntry 默认返回 false, 所以 LinkedHashMap 的插入操作, 默认不会移出元素, 移出元素的操作实际在 LruCache 中的 trimToSize 实现.

在获取和插入之后, LinkedHashMap 中的元素排列就会是: 最近最多使用的位于尾部, 最近最少使用的位于头部.

LruCache 的 trimToSize

trimToSize 目的在于当缓存大于设置的最大内存时, 会移出最近最少使用到的元素(在 LinkedHashMap 中就是头部的元素):

androidxref 上的源码实现:

 

public void trimToSize(int maxSize) {
    while (true) {
        K key;
        V value;
        synchronized (this) {

            if (size <= maxSize) {
                break;
            }

            // 该方法会返回 LinkedHashMap 的头节点
            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);
    }
}

总结

  • Android 提供的 LruCache 基于 LinkedHashMap 实现, 利用 LinkedHashMap 会在每次访问元素之后, 将元素移动到序列末尾的特点, 保证了最近最多使用的元素位于尾部, 最近最少使用的元素位于头部.
    当缓存占用达到设置的上限时, LruCache 就会移出 LinkedHashMap 中的头节点.

  • LinkedHashMap 扩展 HashMap, 实现了一套双向链表机制, 保证了在元素的移动上和元素的查找上的时间复杂度都为O(1)。

链接:https://www.jianshu.com/p/471e03a5d9bf

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值