LruCache了解一下

一、LruCache是什么

该类最开始出现在Android 3.1中,是高速缓存,其中包含对有限数量的值的强引用。每次值被访问时,它都会移到队列的开头。将值添加到缓存中后如果缓存满了,该队列末尾的值将被逐出,并且可能会引发垃圾回收。
默认情况下,缓存大小以条目数衡量。重写 sizeOf以不同的单位调整缓存大小。如下例,此缓存限制存放4MiB的Bitmap:

    int cacheSize = 4 * 1024 * 1024; // 4MiB
    LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount();
       }
    }

二、解析源码

构造方法:

    /**
     * @param maxSize 对于不覆盖sizeOf的缓存,这是缓存中的最大条目数。
     * 对于所有其他缓存,这是此缓存中条目大小的最大和。
     */
    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);
    }

使用了LinkHashMap作为实际缓存的容器,其构造方法的第三个参数是accessOrder,设置为true表示按照访问顺序排序。

public void resize(int maxSize)
设置缓存的大小

LruCache#get
返回key的值(如果它存在于缓存中或可以由create创建)。如果返回值,则将其移到队列的开头。如果未缓存值并且无法创建,则返回null。

    public final V get(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++;
        }

        /*
         * 尝试创建一个值。这可能会花费很长时间,并且当create()返回时,map可能会有所不同。
         * 如果在create()工作时将冲突值添加到map中,则将该值保留在map中并释放创建的值。
         */

        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }

        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);

            if (mapValue != null) {
                // 发生了冲突,因此撤消前面插入的创建的值
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }
    }

LruCache#put
为key缓存value。该值将插入到队列的开头。

    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++;
            size += safeSizeOf(key, value);
            previous = map.put(key, value);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

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

        trimToSize(maxSize);
        return previous;
    }

LruCache#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) {
                    break;
                }

                //调用了LinkedHashMap#eldest来获取最近最少使用的条目
                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);
        }
    }

public final V remove(K key)
删除key条目(如果存在)

protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
当有条目被移除或调用put导致值被替换时会调用该方法,默认是空实现
evicted参数为true表示是缓存满了为了腾出空间而移除的,其它情况为false

protected V create(K key)
当key未命中缓存时调用该方法生成对应key的值,默认返回null

protected int sizeOf(K key, V value)
以用户定义的单位返回key和value的条目大小。默认实现返回1,因此size是条目数,max size是最大条目数。

public final void evictAll()
清除缓存,对每个已删除条目调用entryRemoved

三、LinkedHashMap

最近最少使用的顺序是由LinkedHashMap来确保的,使用了一个双向链表

    /**
     * 双向链表的头(最老)
     */
    transient LinkedHashMapEntry<K,V> head;

    /**
     * 双链表的末尾(最小)
     */
    transient LinkedHashMapEntry<K,V> tail;

    /**
     * 此LinkedHashMap的迭代排序方法:true 表示访问顺序, false 表示插入顺序。
     * @serial
     */
    final boolean accessOrder;

前面提到LruCache初始化LinkedHashMap时accessOrder为true,按访问顺序排序。
accessOrder为true的情况下,双向链表的头是最近最少使用的条目,尾是最近最多使用的条目。

我们看看这个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;
    }

在LinkedHashMap#get方法中,accessOrder为true就调用LinkedHashMap#afterNodeAccess

    void afterNodeAccess(Node<K,V> e) { // 将节点移到最后
        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;
        }
    }

在put的过程中,如果HashMap中不存在对应键的条目,就用了newNode来生成新的条目,LinkedHashMap重写了该方法,把新条目连接到链表末尾

    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMapEntry<K,V> p =
            new LinkedHashMapEntry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }

    // 连接到链表末尾
    private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
        LinkedHashMapEntry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    }

前面提到移除条目时调用LinkedHashMap#eldest获取最近最少使用条目,该方法直接返回了head

    public Map.Entry<K, V> eldest() {
        return head;
    }

四、实现一个简易LruCache

import java.util.LinkedHashMap;
import java.util.Map;

public class LruCache<K,V> extends LinkedHashMap<K,V>{
    int cacheSize;
    public LruCache(int cacheSize){
        super((int) Math.ceil(cacheSize / 0.75),0.75f,true);
        this.cacheSize = cacheSize;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size()>cacheSize;
    }
}
  1. 继承了LinkedHashMap,调用父类构造方法,设置双向链表按访问顺序排序。

  2. 重写 LinkedHashMap#removeEldestEntry 方法,当 size 大于指定缓存容量时,就返回了true,LinkedHashMap就会删掉最旧的元素。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容