Android开发中,为了减少用户的流量使用和使APP体验更流畅,我们通常会使用缓存技术。通常来说,缓存分两级。第一级,是内存缓存,它的好处是,读写非常快,缺点则是,过量地使用会使APP整体变得十分卡顿,因为运行的内存不足了,甚至引起OOM。第二级则是文件缓存(File,SQLite等),文件缓存的读写效率要低于内存缓存。但是空间更加的充足。

  一级缓存由于空间很有限,我们通常会为它设置一个size,当超过这个size时,缓存会将不常用的内容清掉。

  Android中提供了一个方便的容器,用来处理这个缓存问题——LruCache。在此阅读它的源码并做一下笔记。

  

public class LruCache<K, V> {
    private final LinkedHashMap<K, V> map;

    /** Size of this cache in units. Not necessarily the number of elements. */
    private int size;
    private int maxSize;

    private int putCount;
    private int createCount;
    private int evictionCount;
    private int hitCount;
    private int missCount;

  从这里可以很容易地发现,LruCache本质上就是一个LinkedHashMap,我们对它进行控制,达到上述的效果。先看一下这个类的几个属性。

  size是当前缓存的大小

  maxSize是缓存的最大大小

  后面的属性具体作用在阅读过程中再来理解。

  接下来,我们再看一下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);
    }

 

  这一段代码很简单,就是设置缓存的最大size并新建一个LinkedHashMap的对象。并对输入合法性作了检测,如果其值不大于0,则抛出异常。

 

  接下来是重设maxSize的方法。

    /**
     * Sets the size of the cache.
     * @param maxSize The new maximum size.
     *
     * @hide
     */
    public void resize(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }

        synchronized (this) {
            this.maxSize = maxSize;
        }
        trimToSize(maxSize);
    }

 

  除了重设maxSize外,这个方法在最后还对已有的缓存进行了修整。因为当我们将最大缓存修改的比当前缓存还小时,就会有一部分已有的缓存需要清理。所以,我们接下来看一下,清理的方法

 

    /**
     * @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);
        }
    }

 

  整个方法处在一个死循环中,前面先对输入合法性进行了检查,并且只有在 if (size > maxSize)的情况下,才会进行清理、修整。循环中,我们会通过一个for循环找到LinkedHashMap中的最后一项。上面代码中,找到最后一项的代码并不是最优的,它的编写者是为了保持它与线上版本一至。找到最后一项后,获取它的key和value。从LinkedHashMap中移除它,并计算它能够释放的内存大小,再重新检查,现在是内存状况是否满足maxSize的限定,并调用了一个空方法entryRemoved(我们可以通过继承重写这个方法,进行一些扩展)。如果不满足,继续清理。其中safeSizeof是计算缓存中一项的大小的方法,我们再来看看它:

  

    private int safeSizeOf(K key, V value) {
        int result = sizeOf(key, value);
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
    }
    /**
     * Returns the size of the entry for {@code key} and {@code value} in
     * user-defined units.  The default implementation returns 1 so that size
     * is the number of entries and max size is the maximum number of entries.
     *
     * <p>An entry's size must not change while it is in the cache.
     */
    protected int sizeOf(K key, V value) {
        return 1;
    }

  由上面的代码可以看到,不论我们存什么,LruCache都认为一项存的大小为1。这样,我们在设置大小时,实际设置的是我们要存多少项数据。如果要我们需要它能够真正地反映我们存的内容的大小,我们需要继承并重写sizeOf这个方法。

  至此,就看完了LruCache的一次初始化。接下来,我们来看看它是如何保存数据。

  

    /**
     * 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);
            }
        }

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

        trimToSize(maxSize);
        return previous;
    }

  照常先是输入合法性检查,然后会将新的储存值的size加入到整个LruCache的size中。将新的值放入LinkedHashMap中,并取出老的值。然后将老的值占的size从LruCache整个size中减去。接着,调用空方法entryRemoved,最后,修整缓存的size。并将被取代掉的那个值,返回。

  可以看到,LruCache的put的方法几乎就是LinkedHashMap的扩展。多了输入合法性检查、调整size,修整缓存三部分。这里的重点是,由于LinkedHashMap是一个有序的Map结构,因此,不论是insert还是update,最近操作的数据,都会放在最前面。

  

  然后,我们再来看看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.
         */

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

 

  这个方法比较长,可以分为两部分。第一部分是比较常规的从LinkedHashMap中取值,第二部分则是,如果map中不存在这个值,则新建一个这样的值。但是新建的方法,原类中是一个空方法,需要我们自行继承、重写。

 

综上,我们已经分析了LruCache这个类的大部分方法。总结下来它的工作模式就是,将新建或者更新的数据放在最前面,每次操作后,检查size大小,如果size超过了maxSize,则将最后面的值进行清理,使size回归正常范围。如果我们继承这个类,我们可以比较方便的扩展如下内容:

1、每个数据占用的内存大小的计算

2、清理缓存后的我们调用的方法

3、如果要获取的值,不存在于LruCache中,我们新建它调用的方法。

 

   Done~

  

posted on 2015-12-19 19:42  Fishbonell  阅读(427)  评论(0编辑  收藏  举报