做Android app开发的同学应该都听说过或者用过nostra13的Android-Universal-Image-Loader开源库,它在图片异步加载、缓存和显示等方面提供了强大灵活的框架。之前介绍过的android-smart-image-view开源库跟它比起来,真是小巫见大巫了,不仅在功能上,而且在代码量上都差别很大。当然我们在选取开源库的时候并不是功能越强大越好,一切都要看具体需求,只选取能够满足需求的就行,Less Is More。
Android-Universal-Image-Loader可以到https://github.com/nostra13/Android-Universal-Image-Loader上面获取,至于它的使用方法,作者写了3篇博文进行了详细的介绍
(http://www.intexsoft.com/blog/item/68-universal-image-loader-part-1.html),本文就不再赘述了。首先,让我们先瞄一下Image-Loader库的整体包结构:
可以看到,包结构基本上也是根据功能来命名的,即图片异步加载、缓存和显示以及一些工具类。这篇文章我们先来分析下图片的内存缓存实现原理,内存缓存在介绍smart-image-view开源库时已经接触过,只不过当时的实现很简单,Bitmap以软引用的方式直接放到CurrentHashMap中,没有任何过期删除策略,也没有限制缓存大小等等。Image-Loader库将这些都考虑在内了,是一个较完整的内存缓存实现,使用者可以根据需要选择已经实现的策略,也可以定制自己项目中需要的策略。ImageLoader库大量使用了面向接口设计,面向接口设计方式专注于对象所提供的服务或模块的职责,而不是它们的实现。它明确地将规范视图从实现视图中剥离出来。内存缓存实现代码在memory和memory.impl这两个包中,前者就是规范视图,后者是实现视图。memory包中有MemoryCacheAware接口和BaseMemoryCache和LimitedMemoryCache两个抽象类,加上memory.impl包中的WeakMemoryCache类,类图如下所示:
MemoryCacheAware接口以泛型方式定义了实现缓存所需的基础规约,包括写缓存、读缓存、删除缓存和遍历缓存等,如下所示:
/** * Interface for memory cache * * @author Sergey Tarasevich (nostra13[at]gmail[dot]com) * @since 1.0.0 */ public interface MemoryCacheAware<K, V> { /** * Puts value into cache by key * * @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(K key, V value); /** Returns value by key. If there is no value for key then null will be returned. */ V get(K key); /** Removes item by key */ void remove(K key); /** Returns all keys of cache */ Collection<K> keys(); /** Remove all items from cache */ void clear(); }
接口定义好,一般都会提供一个接口的基础实现类,这个类需要实现接口中的方法,并提供子类可以公用的操作,由smart-image-view这篇文章我们知道,在Android中图片最终表现为Bitmap的实例,为了Bitmap及时释放,一般内存缓存中不会直接存放Bitmap的强引用,而是使用弱引用SoftReference,但在Java中,还存在另外两种引用类型,即WeakReference和PhantomReference,考虑到通用性,在内存缓存的抽象基类BaseMemoryCache中将使用上面三种引用的父类Reference。内存缓存依然使用哈希表来实现。代码如下:
/** Stores not strong references to objects */ private final Map<K, Reference<V>> softMap = Collections.synchronizedMap(new HashMap<K, Reference<V>>());
回顾我们在smart-image-view库中的哈希表定义:
private ConcurrentHashMap<String, SoftReference<Bitmap>> memoryCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>();;
可以发现最大的区别是一个使用Collections.synchronizedMap,一个使用ConcurrentHashMap,两者都是实现线程安全的HashMap,区别在哪里呢?感兴趣的可以自己看JDK源码,或者百度之,这里只给出结论:ConcurrentHashMap的读写性能要比Collections.synchronizedMap高,尽量使用前者。下面就看下BaseMemoryCache的代码吧:
/** * Base memory cache. Implements common functionality for memory cache. Provides object references ( * {@linkplain Reference not strong}) storing. * * @author Sergey Tarasevich (nostra13[at]gmail[dot]com) * @since 1.0.0 */ public abstract class BaseMemoryCache<K, V> implements MemoryCacheAware<K, V> { /** Stores not strong references to objects */ private final Map<K, Reference<V>> softMap = Collections.synchronizedMap(new HashMap<K, Reference<V>>()); @Override public V get(K key) { V result = null; Reference<V> reference = softMap.get(key); if (reference != null) { result = reference.get(); } return result; } @Override public boolean put(K key, V value) { softMap.put(key, createReference(value)); return true; } @Override public void remove(K key) { softMap.remove(key); } @Override public Collection<K> keys() { synchronized (softMap) { return new HashSet<K>(softMap.keySet()); } } @Override public void clear() { softMap.clear(); } /** Creates {@linkplain Reference not strong} reference of value */ protected abstract Reference<V> createReference(V value); }
基本上就是HashMap的操作,由于具体Reference的实现需要看到底用的哪种引用,因此,这里将createReference定义成抽象函数,让BaseMemoryCache的子类去定制实现。
BaseMemoryCache的一个最简单的子类实现是WeakMemoryCache类,它仅仅是实现了createReference抽象方法,返回Bitmap对象的弱引用:
public class WeakMemoryCache extends BaseMemoryCache<String, Bitmap> { @Override protected Reference<Bitmap> createReference(Bitmap value) { return new WeakReference<Bitmap>(value); } }
BaseMemoryCache的另一个子类是LimitedMemoryCache,它也是抽象类,定义了实现内存缓存限制策略的公共操作。既然要限制缓存大小,那么首先需要定义默认的缓存最大值,同时需要维护一个表示当前缓存已占用大小的变量,考虑到多线程问题,这个变量值得增减必须保证是原子的,因此,该变量类型选用AtomicInteger。如下所示:
private static final int MAX_NORMAL_CACHE_SIZE_IN_MB = 16; private static final int MAX_NORMAL_CACHE_SIZE = MAX_NORMAL_CACHE_SIZE_IN_MB * 1024 * 1024; private final int sizeLimit; private final AtomicInteger cacheSize;
同时,为了计算已添加到缓存中的数据大小,需要新增一个指向强引用的数据结构来进行记录,而不是使用BaseMemoryCache中的softMap哈希表,因为softMap中存放的是Reference类,里面的数据可能会被GC回收,用来统计已占用大小时不准确。指向数据强引用的数据结构选用LinkedList,定义如下,同样采用线程安全版本。
/** * Contains strong references to stored objects. Each next object is added last. If hard cache size will exceed * limit then first object is deleted (but it continue exist at {@link #softMap} and can be collected by GC at any * time) */ private final List<V> hardCache = Collections.synchronizedList(new LinkedList<V>());
往LimitedMemoryCache中添加数据时,首先要计算出当前这个数据的大小(getSize()),然后加上已占用缓存大小后,跟缓存最大值相比较,超过缓存最大值时,就需要根据缓存清理策略(removeNext())删除以前的一些数据,直到添加数据后,总占用大小在最大值之内为止。上面提到的getSize()函数和removeNext函数需要子类定制,所以定义成了抽象函数。这是典型的模板方法模式的应用,模板方法模式定义一个操作中算法的步骤,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。下面的put函数中实现的就是算法的步骤,而getSize()和removeNext()函数则是子类可重定义的特定步骤。相关代码如下所示:
@Override public boolean put(K key, V value) { boolean putSuccessfully = false; // Try to add value to hard cache int valueSize = getSize(value); int sizeLimit = getSizeLimit(); int curCacheSize = cacheSize.get(); if (valueSize < sizeLimit) { while (curCacheSize + valueSize > sizeLimit) { V removedValue = removeNext(); if (hardCache.remove(removedValue)) { curCacheSize = cacheSize.addAndGet(-getSize(removedValue)); } } hardCache.add(value); cacheSize.addAndGet(valueSize); putSuccessfully = true; } // Add value to soft cache super.put(key, value); return putSuccessfully; } protected abstract int getSize(V value); protected abstract V removeNext();
接下来就介绍LimitedMemoryCache的几个子类的具体实现,它们分别是FIFOLimitedMemoryCache、LargestLimitedMemoryCache、LRULimitedMemoryCache和UsingFreqLimitedMemoryCache。类图如下:
【FIFOLimitedMemoryCache类】
FIFO算法意思是在缓存超过最大值时,缓存清理策略是将最老的数据清理掉,因此使用队列这样的数据结构就可以很好的实现,在Java中,LinkedList类可以实现这样的功能。
private final List<Bitmap> queue = Collections.synchronizedList(new LinkedList<Bitmap>());
而父类LimitedMemoryCache中也定义了类似的数据结构hardCache专门用于计算缓存的数据大小,只不过它是私有的,子类不能使用。但这样就会造成在FIFOLimitedMemoryCache类中存在两个类似的数据结构,内存占用会变大,事实上,FIFOLimitedMemoryCache中的queue完全可以直接使用父类的hardCache,作者之所以没有直接使用,应该是考虑到代码整体类层次结构清晰的问题,毕竟hardCache专门是用于计算缓存大小的,子类实现方式很多种,很多子类并不需要直接使用hardCache,所以单独一个FIFOLimitedMemoryCache以空间来换取代码结构的清晰也是可以理解的。写缓存和清缓存代码如下:
@Override public boolean put(String key, Bitmap value) { if (super.put(key, value)) { queue.add(value); return true; } else { return false; } } @Override public void remove(String key) { Bitmap value = super.get(key); if (value != null) { queue.remove(value); } super.remove(key); } @Override public void clear() { queue.clear(); super.clear(); }
前面说到的父类总共有3个抽象函数,需要子类予以实现,代码如下:
@Override protected int getSize(Bitmap value) { return value.getRowBytes() * value.getHeight(); } @Override protected Bitmap removeNext() { return queue.remove(0); } @Override protected Reference<Bitmap> createReference(Bitmap value) { return new WeakReference<Bitmap>(value); }
getSize函数中可以看到计算Bitmap占用字节大小的典型方法。removeNext函数中移除队列queue头部的数据,这个数据是最先进入队列的数据。而Bitmap的Reference使用的是弱引用,它和软引用的区别是,弱引用对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域过程中,只要发现只具有弱引用的对象,那么不管当前内存空间是否足够,都会回收弱引用对象占用的内存。这样可以更好的防止出现OutOfMemoryError错误。
【LargestLimitedMemoryCache类】
当缓存超出最大值限制时,清理策略是将占用空间最大的数据清理掉。因此,需要一个维护Bitmap对象到它占用大小的映射,这里使用的还是HashMap:
/** * Contains strong references to stored objects (keys) and last object usage date (in milliseconds). If hard cache * size will exceed limit then object with the least frequently usage is deleted (but it continue exist at * {@link #softMap} and can be collected by GC at any time) */ private final Map<Bitmap, Integer> valueSizes = Collections.synchronizedMap(new HashMap<Bitmap, Integer>());
同理,关键代码还是在父类几个抽象函数的实现,其中getSize()和createReference()函数实现和FIFOLimitedMemoryCache类一样,removeNext函数实现如下:
@Override protected Bitmap removeNext() { Integer maxSize = null; Bitmap largestValue = null; Set<Entry<Bitmap, Integer>> entries = valueSizes.entrySet(); synchronized (valueSizes) { for (Entry<Bitmap, Integer> entry : entries) { if (largestValue == null) { largestValue = entry.getKey(); maxSize = entry.getValue(); } else { Integer size = entry.getValue(); if (size > maxSize) { maxSize = size; largestValue = entry.getKey(); } } } } valueSizes.remove(largestValue); return largestValue; }
基本原理是遍历valueSizes这个哈希表,比较并得到占用空间最大的Bitmap对象,然后删除它即可。这里要注意的一点是遍历时要加入synchronized同步机制。
【LRULimitedMemoryCache类】
LRU即Least Recently Used,缓存清理策略是将最近最少使用的Bitmap对象删除掉。按Java中刚好有这样一个数据结构可以实现这个策略,它就是LinkedHashMap类。
private static final int INITIAL_CAPACITY = 10; private static final float LOAD_FACTOR = 1.1f; /** Cache providing Least-Recently-Used logic */ private final Map<String, Bitmap> lruCache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(INITIAL_CAPACITY, LOAD_FACTOR, true));
LinkedHashMap是HashMap的子类,注意它的构造函数第三个参数accessOrder,它定义了LinkedHashMap的排序模式,accessOrder为true时,表示LinkedHashMap中数据排序按照访问的顺序,当为false时,表示数据排序按照数据插入的顺序。而我们要实现LRU,就需要把accessOrder设置为true,同时,在读缓存时不能像FIFOLimitedMemoryCache类和LargestLimitedMemoryCache类一样使用父类BaseMemoryCache的get方法,而是应该重写该方法如下所示:
@Override public Bitmap get(String key) { lruCache.get(key); // call "get" for LRU logic return super.get(key); }
当accessOrder设置为true时,LinkedHashMap就维护了记录的访问顺序,这时使用Iterator 遍历LinkedHashMap时,先得到的记录肯定就是最近最不常使用的那个记录了,LRU算法水到渠成:
@Override protected Bitmap removeNext() { Bitmap mostLongUsedValue = null; synchronized (lruCache) { Iterator<Entry<String, Bitmap>> it = lruCache.entrySet().iterator(); if (it.hasNext()) { Entry<String, Bitmap> entry = it.next(); mostLongUsedValue = entry.getValue(); it.remove(); } } return mostLongUsedValue; }
【UsingFreqLimitedMemoryCache类】
和LargestLimitedMemoryCache类实现类似,只不过一个是将占用空间最大的记录剔除,一个是将访问次数最少的记录剔除,所用数据结构自然类似:
/** * Contains strong references to stored objects (keys) and last object usage date (in milliseconds). If hard cache * size will exceed limit then object with the least frequently usage is deleted (but it continue exist at * {@link #softMap} and can be collected by GC at any time) */ private final Map<Bitmap, Integer> usingCounts = Collections.synchronizedMap(new HashMap<Bitmap, Integer>());
因为要记录访问次数,所以需要重写父类的get方法,每访问一次,就增加计数值:
@Override public Bitmap get(String key) { Bitmap value = super.get(key); // Increment usage count for value if value is contained in hardCahe if (value != null) { Integer usageCount = usingCounts.get(value); if (usageCount != null) { usingCounts.put(value, usageCount + 1); } } return value; }
removeNext函数实现原理是遍历usingCounts哈希表中的记录,比较它们的访问次数,并选取访问次数最少的一个删除之,代码如下所示,同LargestLimitedMemoryCache类,要注意使用synchronized关键字同步保护。
@Override protected Bitmap removeNext() { Integer minUsageCount = null; Bitmap leastUsedValue = null; Set<Entry<Bitmap, Integer>> entries = usingCounts.entrySet(); synchronized (usingCounts) { for (Entry<Bitmap, Integer> entry : entries) { if (leastUsedValue == null) { leastUsedValue = entry.getKey(); minUsageCount = entry.getValue(); } else { Integer lastValueUsage = entry.getValue(); if (lastValueUsage < minUsageCount) { minUsageCount = lastValueUsage; leastUsedValue = entry.getKey(); } } } } usingCounts.remove(leastUsedValue); return leastUsedValue; }
最后,在memory.impl包中还有几个类是直接实现MemoryCacheAware接口的,我们先来看下他们的类结构图:
从类图可以看到,FuzzyKeyMemoryCache类和LimitedAgeMemoryCache类都实现了MemoryCacheAware接口,同时聚合了MemoryCacheAware类型的对象。熟悉设计模式的同学应该能够一眼看出这个就是装饰者模式的应用。装饰者(Decorator)模式用于动态地给一个对象添加一些额外的职责,就增加功能而言,Decorator模式相比生成子类更为灵活。在Decorator模式的实现中,为了能够实现和原来使用被装饰对象的代码的无缝结合,装饰者类需要实现与被装饰对象相同的接口,同时在装饰者类中转调被装饰对象的功能,在转调的前后添加新的功能。就我们的代码来说,被装饰对象就是实现了MemoryCacheAware接口的类对象,装饰者对象就是FuzzyKeyMemoryCache类和LimitedAgeMemoryCache类的对象。更详细的关于Decorator模式的了解查阅设计模式的书籍。
【FuzzyKeyMemoryCache类】
在之前实现内存缓存的映射时,是一个key对应一个value,而这个装饰者类提供的额外功能是:允许多个key对应同一个value,后面插入的key-value对会覆盖以前存在的key-value对。这个类主要用于Image-Loader库内部使用。在FuzzyKeyMemoryCache类的实现中,使用Comparator类实现多个key是否相等的判断,核心代码在put函数中:
/** * Decorator for {@link MemoryCacheAware}. Provides special feature for cache: some different keys are considered as * equals (using {@link Comparator comparator}). And when you try to put some value into cache by key so entries with * "equals" keys will be removed from cache before.<br /> * <b>NOTE:</b> Used for internal needs. Normally you don't need to use this class. * * @author Sergey Tarasevich (nostra13[at]gmail[dot]com) * @since 1.0.0 */ public class FuzzyKeyMemoryCache<K, V> implements MemoryCacheAware<K, V> { private final MemoryCacheAware<K, V> cache; private final Comparator<K> keyComparator; public FuzzyKeyMemoryCache(MemoryCacheAware<K, V> cache, Comparator<K> keyComparator) { this.cache = cache; this.keyComparator = keyComparator; } @Override public boolean put(K key, V value) { // Search equal key and remove this entry synchronized (cache) { K keyToRemove = null; for (K cacheKey : cache.keys()) { if (keyComparator.compare(key, cacheKey) == 0) { keyToRemove = cacheKey; break; } } if (keyToRemove != null) { cache.remove(keyToRemove); } } return cache.put(key, value); } @Override public V get(K key) { return cache.get(key); } @Override public void remove(K key) { cache.remove(key); } @Override public void clear() { cache.clear(); } @Override public Collection<K> keys() { return cache.keys(); } }
【LimitedAgeMemoryCache类】
在前面介绍过的MemoryCacheAware接口实现类中,有时可能需要添加这样一个额外的功能:当缓存的对象存在超过一定时间时,将其清理掉,LimitedAgeMemoryCache这个装饰者类就是实现这样一个功能。首先,实现一个缓存对象key到已存活时间的映射表,在获取缓存对象时,判断该对象是否超过最大存活时间,如果是则将其移除。代码如下所示:
/** * Decorator for {@link MemoryCacheAware}. Provides special feature for cache: if some cached object age exceeds defined * value then this object will be removed from cache. * * @author Sergey Tarasevich (nostra13[at]gmail[dot]com) * @see MemoryCacheAware * @since 1.3.1 */ public class LimitedAgeMemoryCache<K, V> implements MemoryCacheAware<K, V> { private final MemoryCacheAware<K, V> cache; private final long maxAge; private final Map<K, Long> loadingDates = Collections.synchronizedMap(new HashMap<K, Long>()); /** * @param cache Wrapped memory cache * @param maxAge Max object age <b>(in seconds)</b>. If object age will exceed this value then it'll be removed from * cache on next treatment (and therefore be reloaded). */ public LimitedAgeMemoryCache(MemoryCacheAware<K, V> cache, long maxAge) { this.cache = cache; this.maxAge = maxAge * 1000; // to milliseconds } @Override public boolean put(K key, V value) { boolean putSuccesfully = cache.put(key, value); if (putSuccesfully) { loadingDates.put(key, System.currentTimeMillis()); } return putSuccesfully; } @Override public V get(K key) { Long loadingDate = loadingDates.get(key); if (loadingDate != null && System.currentTimeMillis() - loadingDate > maxAge) { cache.remove(key); loadingDates.remove(key); } return cache.get(key); } @Override public void remove(K key) { cache.remove(key); loadingDates.remove(key); } @Override public Collection<K> keys() { return cache.keys(); } @Override public void clear() { cache.clear(); loadingDates.clear(); } }