本篇文章我们来分析一下著名图片加载库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、剔除一个元素的的策略。
总结
其他的内存缓存方式基本类似,总的来概括一下:
只使用的是强引用缓存
- LruMemoryCache(这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用,下面我会从源码上面分析这个类)
使用强引用和弱引用相结合的缓存有
- UsingFreqLimitedMemoryCache(如果缓存的图片总量超过限定值,先删除使用频率最小的bitmap)
- LRULimitedMemoryCache(这个也是使用的lru算法,和LruMemoryCache不同的是,他缓存的是bitmap的弱引用)
- FIFOLimitedMemoryCache(先进先出的缓存策略,当超过设定值,先删除最先加入缓存的bitmap)
- LargestLimitedMemoryCache(当超过缓存限定值,先删除最大的bitmap对象)
- LimitedAgeMemoryCache(当 bitmap加入缓存中的时间超过我们设定的值,将其删除)
只使用弱引用缓存
- WeakMemoryCache(这个类缓存bitmap的总大小没有限制,唯一不足的地方就是不稳定,缓存的图片容易被回收掉)
关于作者
专注于 Android 开发多年,喜欢写 blog 记录总结学习经验,blog 同步更新于本人的公众号,欢迎大家关注,一起交流学习~
在这里插入图片描述
网友评论