美文网首页
Android UIL图片加载缓存源码分析-内存缓存

Android UIL图片加载缓存源码分析-内存缓存

作者: crazyandcoder | 来源:发表于2019-11-12 09:40 被阅读0次

    本篇文章我们来分析一下著名图片加载库Android-Universal-Image-Loader的图片缓存源码。

    源码环境

    版本:V1.9.5
    GitHub链接地址:https://github.com/nostra13/Android-Universal-Image-Loader
    

    我们一般去加载大量的图片的时候,都会做缓存策略,缓存又分为内存缓存和硬盘缓存 ,使用的内存缓存是LruCache这个类,LRU是Least Recently Used 近期最少使用算法,我们可以给LruCache设定一个缓存图片的最大值,它会自动帮我们管理好缓存的图片总大小是否超过我们设定的值, 超过就删除近期最少使用的图片,而作为一个强大的图片加载框架,Universal-Image-Loader自然也提供了多种图片的缓存策略,下面就来详细的介绍下。

    现在我们来看Universal-Image-Loader有哪些内存缓存策略

    内存缓存

    首先我们来了解下什么是强引用和什么是弱引用?

    强引用是指创建一个对象并把这个对象赋给一个引用变量, 强引用有引用变量指向时永远不会被垃圾回收。即使内存不足的时候宁愿报OOM也不被垃圾回收器回收,我们new的对象都是强引用。

    弱引用通过weakReference类来实现,它具有很强的不确定性,如果垃圾回收器扫描到有着WeakReference的对象,就会将其回收释放内存。

    下面我们来依次分析上图中涉及到的所有类的源码。

    内存缓存-> MemoryCache.java (接口)

    /**
     * Interface for memory cache
     *
     * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
     * @since 1.9.2
     */
    public interface MemoryCache {
        /**
         * Puts value into cache by key
         * 通过key将value添加进缓存,成功返回true,失败返回false
         * @return <b>true</b> - if value was put into cache successfully, <b>false</b> - if value was <b>not</b> put into
         * cache
         */
        boolean put(String key, Bitmap value);
        /** 
        * Returns value by key. If there is no value for key then null will be returned. 
        * 通过key获取缓存中的value
        */
        Bitmap get(String key);
        /** 
        * Removes item by key 
        * 通过key删除缓存中的一条数据
        */
        Bitmap remove(String key);
        /** 
        * Returns all keys of cache
        * 返回缓存中所有的key 
        */
        Collection<String> keys();
        /** 
        * Remove all items from cache 
        * 清空缓存
        */
        void clear();
    }
    

    这是一个简单的基础接口,我们接下来要分析的内存缓存类都是实现了这个基础的接口,这个接口很简单,有增加、获取、删除、清空的基本操作,我们继续分析。

    内存缓存-> LruMemoryCache.java (类)

    LruMemoryCache指最近最少使用策略。 一种使用强引用来保存有数量限制的Bitmap的cache(在空间有限的情况,保留最近使用过的Bitmap)。每次Bitmap被访问时,它就被移动到一个队列的头部。当Bitmap被添加到一个空间已满的cache时,在队列末尾的Bitmap会被挤出去并变成适合被GC回收的状态。 注意:这个cache只使用强引用来保存Bitmap。

    /**
     * A cache that holds strong references to a limited number of Bitmaps. Each time a Bitmap is accessed, it is moved to
     * the head of a queue. When a Bitmap is added to a full cache, the Bitmap at the end of that queue is evicted and may
     * become eligible for garbage collection.<br />
     * <br />
     * <b>NOTE:</b> This cache uses only strong references for stored Bitmaps.
     *
     * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
     * @since 1.8.1
     */
    public class LruMemoryCache implements MemoryCache {
        
        //LinkedHashMap用来缓存
        private final LinkedHashMap<String, Bitmap> map;
        //缓存的最大数量
        private final int maxSize;
        
        //当前缓存中的大小
        /** Size of this cache in bytes */
        private int size;
        /** 
        * 初始化,设置缓存的最大容量
        * @param maxSize Maximum sum of the sizes of the Bitmaps in this cache 
        */
        public LruMemoryCache(int maxSize) {
            if (maxSize <= 0) {
                throw new IllegalArgumentException("maxSize <= 0");
            }
            this.maxSize = maxSize;
            this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
        }
        /**
         * 通过key来在内存中返回bitmap,如果存在的话,返回之后并将它移到队列的头部,如果缓存中不存在将返回null
         * Returns the Bitmap for {@code key} if it exists in the cache. If a Bitmap was returned, it is moved to the head
         * of the queue. This returns null if a Bitmap is not cached.
         */
        @Override
        public final Bitmap get(String key) {
            if (key == null) {
                throw new NullPointerException("key == null");
            }
            synchronized (this) {
                return map.get(key);
            }
        }
        /** 缓存bitmap,并移到队列的头部
        * Caches {@code Bitmap} for {@code key}. The Bitmap is moved to the head of the queue. */
        @Override
        public final boolean put(String key, Bitmap value) {
            if (key == null || value == null) {
                throw new NullPointerException("key == null || value == null");
            }
            //同步操作,将bitmap添加进缓存中
            synchronized (this) {
                
                //计算现在当前缓存中的容量大小
                size += sizeOf(key, value);
                //判断是否存在该添加的bitmap,map.put()的返回值如果不为空,说明存在跟key对应的entry,put操作只是更新原有key对应的entry
                Bitmap previous = map.put(key, value);
                
                //如果存在,则删除这个bitmap所占的大小容量
                if (previous != null) {
                    size -= sizeOf(key, previous);
                }
            }
            
            //判断是否需要移除相应的bitmap
            trimToSize(maxSize);
            
            return true;
        }
        /**判断是否超过容量限制,超出则移除相应的bitmap
         * 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.
         */
        private void trimToSize(int maxSize) {
            while (true) {
                String key;
                Bitmap value;
                synchronized (this) {
                    if (size < 0 || (map.isEmpty() && size != 0)) {
                        throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
                    }
                    if (size <= maxSize || map.isEmpty()) {
                        break;
                    }
                    
                    //从头开始迭代,移除bitmap
                    Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
                    if (toEvict == null) {
                        break;
                    }
                    key = toEvict.getKey();
                    value = toEvict.getValue();
                    map.remove(key);
                    size -= sizeOf(key, value);
                }
            }
        }
        /** 
        * 移除
        * Removes the entry for {@code key} if it exists.
        *  
        * */
        @Override
        public final Bitmap remove(String key) {
            if (key == null) {
                throw new NullPointerException("key == null");
            }
            synchronized (this) {
                Bitmap previous = map.remove(key);
                if (previous != null) {
                    size -= sizeOf(key, previous);
                }
                return previous;
            }
        }
        
        /**
        * 获取缓存中所有的key
        */
        @Override
        public Collection<String> keys() {
            synchronized (this) {
                return new HashSet<String>(map.keySet());
            }
        }
        /**
        * 清空缓存
        */
        @Override
        public void clear() {
            trimToSize(-1); // -1 will evict 0-sized elements
        }
        /**
         * 计算每张图片所占的byte数
         * Returns the size {@code Bitmap} in bytes.
         * <p/>
         * An entry's size must not change while it is in the cache.
         */
        private int sizeOf(String key, Bitmap value) {
            return value.getRowBytes() * value.getHeight();
        }
        @Override
        public synchronized final String toString() {
            return String.format("LruCache[maxSize=%d]", maxSize);
        }
    }
    

    我们来看下LruMemoryCache.get(…)方法,在该方法里面,只有一个简简单单的判断,然后调用LinkedHashMap.get(…)方法,我们会好奇,这不是就简简单单将Bitmap从map中取出来吗?但LruMemoryCache声称保留在空间有限的情况下保留最近使用过的Bitmap。不急,让我们细细观察一下map。它是一个LinkedHashMap<String, Bitmap>型的对象。LinkedHashMap中的get()方法不仅返回所匹配的值,并且在返回前还会将所匹配的key对应的entry调整在列表中的顺序(LinkedHashMap使用双链表来保存数据),让它处于列表的最后。当然,这种情况必须是在LinkedHashMap中accessOrder==true的情况下才生效的,反之就是get()方法不会改变被匹配的key对应的entry在列表中的位置。

    LinkedHashMap

    //LinkedHashMap.java
    /**
         * Returns the value of the mapping with the specified key.
         *
         * @param key
         *            the key.
         * @return the value of the mapping with the specified key, or {@code null}
         *         if no mapping for the specified key is found.
         */
        @Override public V get(Object key) {
            /*
             * This method is overridden to eliminate the need for a polymorphic
             * invocation in superclass at the expense of code duplication.
             */
            if (key == null) {
                HashMapEntry<K, V> e = entryForNullKey;
                if (e == null)
                    return null;
                if (accessOrder)
                    makeTail((LinkedEntry<K, V>) e);
                return e.value;
            }
            int hash = Collections.secondaryHash(key);
            HashMapEntry<K, V>[] tab = table;
            for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
                    e != null; e = e.next) {
                K eKey = e.key;
                if (eKey == key || (e.hash == hash && key.equals(eKey))) {
                    if (accessOrder){
                        //调整entry在列表中的位置,其实就是双向链表的调整
                        makeTail((LinkedEntry<K, V>) e);
                    }
                    return e.value;
                }
            }
            return null;
        }
    

    到现在我们就清楚LruMemoryCache使用LinkedHashMap来缓存数据,在LinkedHashMap.get()方法执行后,LinkedHashMap中entry的顺序会得到调整。那么我们怎么保证最近使用的项不会被剔除呢?接下去,让我们看看LruMemoryCache.put(…)。在该方法里面,map.put(key,value)的返回值如果不为空,说明存在跟key对应的entry,put操作只是更新原有key对应的entry而已。trimToSize(…)这个函数就是用来限定LruMemoryCache的大小不要超过用户限定的大小,cache的大小由用户在LruMemoryCache刚开始初始化的时候限定。这个函数做的事情也简单,遍历map,将多余的项(代码中对应toEvict)剔除掉,直到当前cache的大小等于或小于限定的大小。

    这时候我们会有一个以为,为什么遍历一下就可以将使用最少的bitmap缓存给剔除,不会误删到最近使用的bitmap缓存吗?首先,我们要清楚,LruMemoryCache定义的最近使用是指最近用get或put方式操作到的bitmap缓存。其次,之前我们直到LruMemoryCache的get操作其实是通过其内部字段LinkedHashMap.get(…)实现的,当LinkedHashMap的accessOrder==true时,每一次get或put操作都会将所操作项(图中第3项)移动到链表的尾部(见下图,链表头被认为是最少使用的,链表尾被认为是最常使用的。),每一次操作到的项我们都认为它是最近使用过的,当内存不够的时候被剔除的优先级最低。需要注意的是一开始的LinkedHashMap链表是按插入的顺序构成的,也就是第一个插入的项就在链表头,最后一个插入的就在链表尾。假设只要剔除图中的1,2项就能让LruMemoryCache小于原先限定的大小,那么我们只要从链表头遍历下去(从1→最后一项)那么就可以剔除使用最少的项了。

    至此,我们就知道了LruMemoryCache缓存的整个原理,包括他怎么put、get、剔除一个元素的的策略。

    总结

    其他的内存缓存方式基本类似,总的来概括一下:

    只使用的是强引用缓存

    1. LruMemoryCache(这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用,下面我会从源码上面分析这个类)

    使用强引用和弱引用相结合的缓存有

    1. UsingFreqLimitedMemoryCache(如果缓存的图片总量超过限定值,先删除使用频率最小的bitmap)
    2. LRULimitedMemoryCache(这个也是使用的lru算法,和LruMemoryCache不同的是,他缓存的是bitmap的弱引用)
    3. FIFOLimitedMemoryCache(先进先出的缓存策略,当超过设定值,先删除最先加入缓存的bitmap)
    4. LargestLimitedMemoryCache(当超过缓存限定值,先删除最大的bitmap对象)
    5. LimitedAgeMemoryCache(当 bitmap加入缓存中的时间超过我们设定的值,将其删除)

    只使用弱引用缓存

    1. WeakMemoryCache(这个类缓存bitmap的总大小没有限制,唯一不足的地方就是不稳定,缓存的图片容易被回收掉)

    关于作者

    专注于 Android 开发多年,喜欢写 blog 记录总结学习经验,blog 同步更新于本人的公众号,欢迎大家关注,一起交流学习~

    在这里插入图片描述

    相关文章

      网友评论

          本文标题:Android UIL图片加载缓存源码分析-内存缓存

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