理解 LruCache 机制

1. 概述

由于 Android 为每个进程分配的可用内存空间都是有限的,如果进程使用的内存超过了所分配的限制就会出现内存溢出问题。同时,如果应用每使用一个资源都需要从本地或网络加载,这无疑会影响应用的性能,为了既能保证应用性能又能避免内存溢出,就出现内存缓存技术

所谓内存缓存技术指的是把一些资源缓存在内存中,如果需要加载资源,首先到内存中去寻找,寻找到的话就直接使用,否则去本地或者网络去寻找。其中最重要的是内存缓存技术要有一个合适的缓存策略,即根据什么策略把缓存中的资源删除,以保证缓存空间始终在一个合理的范围内。

LruCacheAndroid提供的一个标准的基于LRU,最近最少使用算法的缓存技术,它的使用方法已经在其他博文里简单介绍过了,这里主要介绍它的实现机制。

2. LruChche 实现原理

LRU的全称是Least Recently Used,最近最少使用LruCache的实现原理就是在其内部维护一个队列,内部元素按照最近使用时间进行排序,队首是最近最常使用的元素,队尾是最近最少使用的元素,当缓存中元素达到最大数量后,把最近最少使用的元素即队尾元素从缓存队列中移除,从而保证缓存始终在一个合理内存范围内。

下图简单演示LruCache的过程:

LruCache 演示图

从这个演示图中可以发现:

  1. 每次新入队的元素总是位于队首;
  2. 队尾元素是最久没有使用过的元素;
  3. 当队列中的元素被再次使用后,就会把该元素重新插入到队首。

LruCache中使用LinkedHashMap来保存元素,而 LinkedHashMap内部使用双向链表来实现这样的一个 LRU队列,其具体实现在这里就不详细描述了,大家只要了解这点就可以了。

3. LruCache 关键实现

内存缓存技术中最关键的实现主要包含三部分:

  • 如何把元素加入缓存
  • 如何从缓存中获取元素
  • 如何在缓存满时删除元素

3.1 LruCache 的初始化

在详细讲解LruCache的三个关键实现部分前,首先要知道LruCache 的初始化。
首先看下是如何在代码里使用LruCache的:

    int maxMemory = (int) Runtime.getRuntime().maxMemory();
    LruCache<String, Bitmap> mCache = new LruCache<String, Bitmap>(maxMemory / 4) {
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount();
        }
    };

在这段示例代码里,创建了一个LruCache示例并重写了sizeOf方法。重写sizeOf方法是因为它会被用来判断缓存的当前大小是否已经达到了预定义的缓存大小,如果超过就需要从中移除最久没有使用的元素。默认情况下sizeOf返回的时候元素个数,所以如果在创建LruCache时指定的缓存中的元素个数而非内存空间就可以不重新sizeOf方法。

现在来看在创建LruCache的时候到底发生了什么,其构造函数如下:

    /**
     * @param maxSize for caches that do not override {@link #sizeOf}, this is
     *     the maximum number of entries in the cache. For all other caches,
     *     this is the maximum sum of the sizes of the entries in this cache.
     */
    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);
    }

从构造函数里发现,除了根据传入的参数确定了缓存的最大内存空间(也可能是元素数量)外,还定义了一个LinkedHashMap并把其中的第三个参数设置为trueLinkedHashMap的构造函数如下:

    /**
     * Constructs an empty <tt>LinkedHashMap</tt> instance with the
     * specified initial capacity, load factor and ordering mode.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @param  accessOrder     the ordering mode - <tt>true</tt> for
     *         access-order, <tt>false</tt> for insertion-order
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

其中,参数分别是初始容量, 负载因子和排序方式,如果accessOrder被设置为true就表示是按照访顺序进行排序的,这也就保证了LruCache中的原生是按照访问顺序排序的。

所以在LruCache的初始化过程中,一方面确定了缓存的最大空间,另一方面利用LinkedHashMap实现了LRU队列。

3.2 LruCache 缓存元素

要使用LruCache,首先需要把需要缓存的资源加入到LruCache缓存空间,在LruCache实现这一功能的是put接口,来看下是如何实现的:

    /**
     * Caches {@code value} for {@code key}. The value is moved to the head of
     * the queue.
     *
     * @return the previous value mapped by {@code key}.
     */
    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);
            }
        }
        // 如果是更新元素,需要发出通知,默认 entryRemoved 没有实现。
        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }
        // 检查缓存大小是否达到限制,如果达到需要移除最久没使用的元素。
        trimToSize(maxSize);
        return previous;
    }

put方法整体逻辑比较简单,就是把新元素放在队首,更新当前缓存大小,并使用trimToSize 来保证当前缓存大小没有超过限制,其代码如下:

    /**
     * @param maxSize the maximum size of the cache before returning. May be -1
     *     to evict even 0-sized elements.
     */
    private 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;
                }

                // BEGIN LAYOUTLIB CHANGE
                // get the last item in the linked list.
                // This is not efficient, the goal here is to minimize the changes
                // compared to the platform version.
                Map.Entry<K, V> toEvict = null;
                for (Map.Entry<K, V> entry : map.entrySet()) {
                    toEvict = entry;
                }
                // END LAYOUTLIB CHANGE

                if (toEvict == null) {
                    break;
                }

                // 找到对稳元素,即最久没有使用的元素,并移除之。
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                // 移除元素后更新当前大小
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }

trimToSize的逻辑也很简单明了,在缓存队列中找到最近最久没有使用的元素,把它从队列中移除,直到缓存大小满足限制。由于最近最久没有使用的元素一直位于队尾,所以只要找到队尾元素并把它移除即可。

3.3 LruCache 取元素

缓存元素的最终目的是为了方便后续能从缓存中更快地获取需要元素,LruCache获取元素是通过get方法来实现的,其代码如下:

    /**
     * Returns the value for {@code key} if it exists in the cache or can be
     * created by {@code #create}. If a value was returned, it is moved to the
     * head of the queue. This returns null if a value is not cached and cannot
     * be created.
     */
    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++;
        }

        /*
         * 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.
         */
        // 如果找不到元素就调用 create 去创建一个元素,默认 create 返回 null.
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }

        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);
            // 新创建的元素和队列中已存在元素冲突,这个已存在元素是在 create的过程中新加入队列的。
            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;
        }
    }

get方法的逻辑也是很简洁明了的,就是直接从缓存队列中获取元素,如果查找到就返回并更新元素位置到队首,如果查不到就自己创建一个加入队列,但考虑到多线程的情况,加入队列是需要考虑冲突情况。

3.4 LruCache 移除元素

虽然LruCache可以在缓存空间达到限制是自动把最近最久没使用的元素从队列中移除,但也可以主动去移除元素,使用的方法就是remove,其代码如下:

    /**
     * Removes the entry for {@code key} if it exists.
     *
     * @return the previous value mapped by {@code key}.
     */
    public final V remove(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;
    }

remove的逻辑更加简单,到缓存队列中找到元素,移除,并更新缓存大小即可。

4. 总结

本文主要分析了LruCache的内部实现机制,由于LruCache本身的代码量比较小,分析起来难度也不大,但养成分析源码的习惯所代表的意义更大,让我们一起 Reading The Fucking Source Code !

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

推荐阅读更多精彩内容