zoukankan      html  css  js  c++  java
  • LruCache源码解析

      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~

      

  • 相关阅读:
    5月做题计划(数据结构)
    SRM 545 DIV2
    6月做题计划(递归与分治)
    POJ 3121 The SetStack Computer
    struts2初步学习路线
    my97datepicker日历展示出现中文乱码的问题
    tomcat请求数据的编码设置
    STRUT2传递参数中文乱码解决方法
    js mine 类型javascripttext/javascript,application/javascript, and appliation/xjavascript
    eclipse内存设置参数
  • 原文地址:https://www.cnblogs.com/fishbone-lsy/p/5059640.html
Copyright © 2011-2022 走看看