为什么我要写一个已经过时框架的解析框架呢?
那在我自己的项目里面其实也不用这个框架了,一般用的是Glide框架,那为什么我要写这篇文章呢,是因为由于我目前水平有限,对于Glide的源码难以参透,那与其在一个高大上的框架上钻牛角尖,不如把已有的框架进行一个全面的解析,因为图片解析框架无非就是如何高效的加载图片,若参透了之前的框架,那么对新的框架新的技术,也就可以了解其本质,从而更游刃有余的去学习新的技术。
UIL的简单使用
首先导入jar包,在Application的onCreate方法中对UIL进行一些参数配置,如果不想配置,也可以直接使用默认的参数配置。
那么最常用的加载图片方法:
private void showImage() {
ImageLoader loader = ImageLoader.getInstance();
//最常用的方法
// loader.displayImage(url,iv);
//同样我们配置些参数
DisplayImageOptions options = new DisplayImageOptions.Builder()
//缓存到内存中
.cacheInMemory(true)
//缓存到磁盘中
.cacheOnDisk(true)
.build();
loader.displayImage(url,iv,options);
}
那么以上就是UIL的基本使用了。
内存缓存
我们一般去加载大量的图片的时候,都会做缓存策略,缓存又分为内存缓存和硬盘缓存。UIL默认使用的内存缓存是LruCache这个类,LRU是Least Recently Used 近期最少使用算法,我们可以给LruCache设定一个缓存图片的最大值,它会自动帮我们管理好缓存的图片总大小是否超过我们设定的值, 超过就删除近期最少使用的图片,而作为一个强大的图片加载框架,Universal-Image-Loader自然也提供了多种图片的缓存策略,下面就来详细的介绍下。
那么首先来分析一下LruCache干了些什么事
package com.nostra13.universalimageloader.cache.memory.impl;
import android.graphics.Bitmap;
import com.nostra13.universalimageloader.cache.memory.MemoryCacheAware;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 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 MemoryCacheAware<String, Bitmap> {
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);
}
/**
* 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);
}
}
/** 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");
}
synchronized (this) {
size += sizeOf(key, value);
Bitmap previous = map.put(key, value);
if (previous != null) {
size -= sizeOf(key, previous);
}
}
trimToSize(maxSize);
return true;
}
/**
* 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;
}
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 void 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);
}
}
}
@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
}
/**
* 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的初始化方法中初始化了一个LinkedHashMap和一个MaxSize,LinkedHashMap传入了true,是访问优先的模式,MaxSize代表了我们想要在内存中缓存所有图片的最大比特。还定义了一个size表示当前的所有图片大小。那么在put方法中,首先调用了一个sizeof()方法,sizeof()方法是用来计算put进去的图片大小,那么size会加上这个sizeof()的值,并且将bitmap放入map中,如果map已经缓存过了,就要减去之前计算得出的size。之后就会调用一个trimtosize(maxsize)方法,这个方法是用来调整map中图片的个数,如果size<=maxsize,那么什么都不做,如果反之,就删除map中的第一个元素,并循环这个过程,直到size<=maxsize。这就是整个LruMemoryCache的原理,并不复杂。
那么我们可以看到最为核心的类是LinkedHashMap,那LinkedHashMap是如何实现Lru算法的呢?
LinkedHashMap解析
LinkedHashMap是HashMap的一个子类,它保留插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap。
LinkedHashMap是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
LinkedHashMap实现与HashMap的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。
注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。
根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用get方法)的链表。
默认是按插入顺序排序,如果指定按访问顺序排序,那么调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表。可以重写removeEldestEntry方法返回true值指定插入元素时移除最老的元素。 让我们来看一下源码
/**
* 双向链表的表头元素。
*/
private transient Entry<K,V> header;
/**
* LinkedHashMap的Entry元素。
* 继承HashMap的Entry元素,又保存了其上一个元素before和下一个元素after的引用。
*/
private static class Entry<K,V> extends HashMap.Entry<K,V> {
Entry<K,V> before, after;
……
}
// 通过源代码可以看出,在LinkedHashMap的构造方法中,实际调用了父类HashMap的相关构造方法来构造一个底层存放的table数组,并默认是插入顺序
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
//LinkedHashMap重写了init()方法,来完成对header的初始化
void init() {
header = new Entry<K,V>(-1, null, null, null);
header.before = header.after = header;
}
/* LinkedHashMap并未重写父类HashMap的put方法,而是重写了父类HashMap的put方法调用的子方法void recordAccess(HashMap m) ,void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了自己特有的双向链接列表的实现。*/
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
//如果为访问优先,那么删除当前节点,添加到链表尾部
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
void addEntry(int hash, K key, V value, int bucketIndex) {
// 调用create方法,将新元素以双向链表的的形式加入到映射中。
createEntry(hash, key, value, bucketIndex);
// 删除最近最少使用元素的策略定义
Entry<K,V> eldest = header.after;
//如果需要删除节点,那么就删除,如果不需要,则判断一下是否要扩容
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
} else {
if (size >= threshold)
resize(2 * table.length);
}
}
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
table[bucketIndex] = e;
// 调用元素的addBrefore方法,将元素加入到哈希、双向链接列表。
e.addBefore(header);
size++;
}
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
那么总结一下put方法,如果map中已经有元素存在,并且是访问优先,就删除当前链表上的节点,并将当前节点添加到链表的尾部。
如果不存在,就直接添加到链表的尾部,并判断是否要移除最老的元素,如果需要移除,那么就直接移除,如果不需要移除也判断是否需要扩容。
LinkedHashMap重写了父类HashMap的get方法,实际在调用父类getEntry()方法取得查找的元素后,再判断当排序模式accessOrder为true时,记录访问顺序,将最新访问的元素添加到双向链表的表头,并从原来的位置删除。由于的链表的增加、删除操作是常量级的,故并不会带来性能的损失。
OK,对LinkedHashMap是如何实现LruCache算法也有些了解。
那么现在了解了些基本原理,我们来梳理一下UIL的工作流程。
- UI:请求数据,使用唯一的Key值索引Memory Cache中的Bitmap。
- 内存缓存:缓存搜索,如果能找到Key值对应的Bitmap,则返回数据。否则执行第三步
- 硬盘存储:使用唯一Key值对应的文件名,检索SDCard上的文件
- 如果有对应文件,使用BitmapFactory.decode*方法,解码Bitmap并返回数据,同时将数据写入缓存。如果没有对应文件,执行第五步
- 下载图片:启动异步线程,从数据源下载数据(Web)
- 若下载成功,将数据同时写入硬盘和缓存,并将Bitmap显示在UI中