理解 LruCache 机制

本人只是 Android小菜一个,写技术文档只是为了总结本身在最近学习到的知识,历来不敢为人师,若是里面有些不正确的地方请你们尽情指出,谢谢!java

1. 概述

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

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

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

2. LruChche 实现原理

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

下图简单演示LruCache的过程: app

LruCache 演示图
从这个演示图中能够发现:

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

LruCache中使用LinkedHashMap来保存元素,而 LinkedHashMap内部使用双向链表来实现这样的一个 LRU队列,其具体实如今这里就不详细描述了,你们只要了解这点就能够了。ide

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 !

相关文章
相关标签/搜索