美文网首页程序员
12.图片三级缓存和LruCache源码

12.图片三级缓存和LruCache源码

作者: 任振铭 | 来源:发表于2018-05-19 16:54 被阅读113次

    大多的开源图片框架针对图片加载都采用了三级缓存的方式,大概流程通常是这样的,加载图片时,首先检查内存中是否仍然保有这个图片对象,如果有则直接显示到控件上,加载过程到此结束;如果内存中没有,则可能是第一次加载,还没有缓存或者内存中的缓存被销毁,这时候去本地缓存中读取,通常是写入到了文件中,如果文件中读取到了缓存,则设置给控件显示,加载结束,如果没有缓存,则再请求服务器返回,这时候会将获取到的图片写入到本地硬盘(文件)中,(或者同时在内存中也写入一份),同时设置给图片显示。这时第一次加载结束,下次再次加载时,重复上一个过程,只要存在缓存就不再请求网络,降低不必要的网络请求。如下图

    图片三级缓存.png
    简易版的三级缓存框架:https://github.com/renzhenming/BitmapUtils

    内存缓存是如何实现的,我们当然可以用一个HashMap来存储获取到的bitmap,以url的md5值为key来保存,但是有一个问题需要注意,安卓系统为每一个应用分配的内存都是有限的,使用HashMap固然可以实现功能,但是当图片足够多的时候,HashMap无法为你清理内存,极有可能发生内存溢出。

    为了防止这种问题,可以在把bitmap加如到集合中时,使用软引用,弱引用,虚引用等包裹bitmap,这样可以防止内存溢出,及时的清理bitmap,但有一个问题,这样内存缓存的作用就不存在了,我们的目的是做缓存,但从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,软引用和弱引用已经不再可靠了,试想如果你刚刚加入缓存就被系统清理了,达不到我们想要的效果,所以Android系统提供了一个可靠的缓存集合LruCache,LruCache内部封装了一个LinkedHashMap集合,所以也可以把它当作集合来看待

            long maxMemory = Runtime.getRuntime().maxMemory();//获取Dalvik 虚拟机最大的内存大小:16
    
            LruCache<String, Bitmap> lruCache = new LruCache<String,Bitmap>((int) (maxMemory/8)){//指定内存缓存集合的大小
                //获取图片的大小
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    return value.getRowBytes()*value.getHeight();
                }
            };
    

    LruCache创建的时候通过泛型指定map集合的key和value类型,并且通过构造方法传入设定的内存缓存集合的最大值,我们来看看它的构造方法,可以看到,这里保存了内存的最大占用空间,并且创建了一个LinkedHashMap,相比于HashMap,保证有序性

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

    当网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) {
                //每加如一个缓存对象,缓存计数器增加1
                putCount++;
                //计算出当前缓存的大小值,safeSizeOf就是上边重写的sizeOf
                //方法的封装,得到的是当前加如的缓存对象的大小,然后累加得到总大小
                size += safeSizeOf(key, value);
                //如果缓存中存在相同的对象,总的缓存大小减去当前这个存入的大小
                //也就是重复的缓存对象不计入缓存,map集合在put的时候,如果集合中存在的
                //话会用新的value值替换旧的value,不存在重复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;
        }
    

    trimToSize
    每次向LruCache中添加新的对象缓存时,都会检查一次当前缓存大小是否超过了设置的最大值,这是一个死循环,只要占用的空间大小极值,就会一直根据lru算法来得到最符合移除条件的一个对象然后移除它,直到内存大小在合理范围内

    /**
         * Remove the eldest entries until the total of remaining entries is at or
         * below the requested size.
         *
         * @param maxSize the maximum size of the cache before returning. May be -1
         *            to evict even 0-sized elements.
         */
        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;
                    }
                    //根据lru算法(Least recently used,最近最少使用)得到一个entry
                    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);
            }
        }
    

    看到这里可以知道,其实LruCache的关键在于LinkedHashMap内部是如何运转的,它会根据lru算法,获取到最符合移除条件的一个对象,eldest()方法返回的就是我们需要的值,那么究竟是如何判断的呢,只有进入源码去看一下了

    在LruCache实例化的时候,我们看到LinkedHashMap是这样构建的,关键在于最后一个true的标记,这个值代表什么?

     this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    

    LinkedHashMap有五种构造方法,前四个构造方法都将accessOrder设为false,默认是按照插入顺序排序的;而第五个构造方法可以自定义传入的accessOrder的值,因此可以指定双向循环链表中元素的排序规则。特别地,当我们要用LinkedHashMap实现LRU算法时,就需要调用该构造方法并将accessOrder置为true。本质上,LinkedHashMap = HashMap + 双向链表,可以这样理解,LinkedHashMap 在不对HashMap做任何改变的基础上,给HashMap的任意两个节点间加了两条连线(before指针和after指针),使这些节点形成一个双向链表。在LinkedHashMapMap中,所有put进来的Entry都保存在HashMap中,并且对于每次put进来Entry还会将其插入到双向链表的尾部。

    我们看看设置为true或者false,链表的存取有什么区别,找到put方法,LinkedHashMap的put方法使用的是父类HashMap的put

    public V put(K key, V value) {
           return putVal(hash(key), key, value, false, true)
      }
    
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            else {
                Node<K,V> e; K k;
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                else if (p instanceof TreeNode)
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {
                    for (int binCount = 0; ; ++binCount) {
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                //这里可以看到,当存在相同的key时,会将新的value替换旧的value,并将旧的返回
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    //走到了这个方法,但其实这个方法在HashMap中是空的,LinkedHashMap重写了这个
                    //方法,我们看看它是如何实现
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
    

    可以看到,LinkedHashMap调用put方法添加数据的时候,除了继承了HashMap把数据添加到hash表中,还做了另一步操作,就是同时加入了它内部的一个双向链表中,并且是加入了尾部,如果是第一个加如得数据自然也就是头部了,那么这时差不多明白了,LinkedHashMap方法得eldest方法返回得值怎么就是最近最少使用得呢

    void afterNodeAccess(Node<K,V> e) { // move node to last
            LinkedHashMapEntry<K,V> last;
            //这里是构造LinkedHashMap时传入的标记位,只有为true的情况才会加如链表,
            //也就是说只有第五种构造方法被调用并且accessOrder 赋值为true,链表才会起作用
            //同时判断一个条件,当前要加如链表的value和当前链表中最后一个value是否相同
            //只有不同的情况才会将其加入
            if (accessOrder && (last = tail) != e) {
                //注意这种结构,p b a都是LinkedHashMapEntry类型,这一行代码写的很简洁
                //第一,将e强转为LinkedHashMapEntry p,第二将p在链表中的位置放在b的后边
                //(b = p.before),将p的后边指定为a,这里可以猜想b就是上一次放入链表中的
                //LinkedHashMapEntry
                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;
            }
        }
    

    上边是存入得时候,那么取出得时候呢

    /**
         * Returns the value to which the specified key is mapped,
         * or {@code null} if this map contains no mapping for the key.
         *
         * <p>More formally, if this map contains a mapping from a key
         * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
         * key.equals(k))}, then this method returns {@code v}; otherwise
         * it returns {@code null}.  (There can be at most one such mapping.)
         *
         * <p>A return value of {@code null} does not <i>necessarily</i>
         * indicate that the map contains no mapping for the key; it's also
         * possible that the map explicitly maps the key to {@code null}.
         * The {@link #containsKey containsKey} operation may be used to
         * distinguish these two cases.
         */
        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存取数据都会调用afterNodeAccess方法,将最近使用得value放在链表得末尾,那么这样一来就很清楚了,使用最多得一直放在链表得末尾,使用最少得自然就放在头部了,那么eldest方法返回得head自然也就是最满足移除条件得最少使用得value了。

    LinkedHashMap重写了HashMap中的afterNodeAccess方法(HashMap中该方法为空),当调用父类的put方法时,在发现key已经存在时,会调用该方法;当调用自己的get方法时,也会调用到该方法。该方法提供了LRU算法的实现,它将最近使用的Entry放到双向循环链表的尾部。也就是说,当accessOrder为true时,get方法和put方法都会调用recordAccess方法使得最近使用的Entry移到双向链表的末尾。

    此时可以回到最初得位置了,LruCache中通过这一行代码取得要移除得对象,从而保证将内存控制在合理范围内。最根本的实现在于LinkedHashMap。

     Map.Entry<K, V> toEvict = map.eldest();
    

    相关文章

      网友评论

        本文标题:12.图片三级缓存和LruCache源码

        本文链接:https://www.haomeiwen.com/subject/fchadftx.html