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

2019-11-12  本文已影响0人  crazyandcoder

本篇文章我们来分析一下著名图片加载库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 同步更新于本人的公众号,欢迎大家关注,一起交流学习~

在这里插入图片描述
上一篇下一篇

猜你喜欢

热点阅读