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

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

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值