Universal-Image-Loader解析系列
Universal-Image-Loader解析(一)基本介绍与使用
Universal-Image-Loader解析(二)内部缓存原理
Universal-Image-Loader解析(三)源代码解析
前篇文章则跟大家介绍了UIL的一些常用用法。
对于我们所知道的缓存,常用的是内存缓存MemoryCache和硬盘缓存DiscCache。一个读取快容量小,一个读取慢容量大。
对于各自使用哪种缓存,则可以在前面配置ImageLoaderConfiguration
进行缓存设置,当然也可以自己自定义适合的缓存。
ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(this)
.memoryCache(new WeakMemoryCache())
.build();
对于Universal-Image-Loader来说它的缓存结构也是分为内存缓存MemoryCache和硬盘缓存DiskCache
一.MemoryCache内存缓存
首先先看个结构图,理解UIL里面内存缓存的结构
由于空间有限就没画成标准的UML类图形式。
对于基类MemoryCache
它则是一个接口,里面定义了put,get图片的方法
public interface MemoryCache {
...
boolean put(String key, Bitmap value);
Bitmap get(String key);
Bitmap remove(String key);
Collection<String> keys();
void clear();
}
都是大家比较所熟悉的方法,而对于其他的类
我们一个个看
LruMemoryCache
这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用。直接实现了MemoryCache
方法
public class LruMemoryCache implements MemoryCache {
private final LinkedHashMap<String, Bitmap> map;
//最大容量
private final int maxSize;
/** 目前缓存的容量大小 */
private int size;
public LruMemoryCache(int maxSize) {
...
this.maxSize = maxSize;
this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
}
@Override
public final Bitmap get(String key) {
...
synchronized (this) {
return map.get(key);
}
}
@Override
public final boolean put(String key, Bitmap value) {
...
synchronized (this) {
size += sizeOf(key, value);
Bitmap previous = map.put(key, value);
if (previous != null) {
size -= sizeOf(key, previous);
}
}
trimToSize(maxSize);
return true;
}
/**
* Lru算法,当容量超过最大缓存容量,则移除最久的条目
*/
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);
}
}
}
@Override
public final Bitmap remove(String key) {
...
synchronized (this) {
Bitmap previous = map.remove(key);
if (previous != null) {
size -= sizeOf(key, previous);
}
return previous;
}
}
...
//返回图片的字节大小
private int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
...
}
LruMemoryCache
的源码也比较简单,内部有个成员变量LinkedHashMap<String, Bitmap> map
这里直接进行保存的话则是强引用的形式。
主要看get,put方法。
对于get方法来说,比较简单,直接根据指定的key返回对应的图片。
而对于put方法来说,则需要考虑容量的问题。
@Override
public final boolean put(String key, Bitmap value) {
...
synchronized (this) {
size += sizeOf(key, value);
Bitmap previous = map.put(key, value);
if (previous != null) {
size -= sizeOf(key, previous);
}
}
trimToSize(maxSize);
return true;
}
put方法首先调用了sizeof
方法,该方法则是返回指定Bitmap的字节大小,之后size +=,总缓存量增加,之后调用trimToSize
该方法则是进行缓存容量判断的。
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);
}
}
}
如果加入后的size 缓存容量 <= maxSize 最大缓存容量,则直接break,不用进行判定处理。
如果大于的话,则直接移除最久未使用的。
大家肯定有疑问,它到底怎么判断最久未使用的?没看到相关代码呀?
相信知道LinkedHashMap
的话可能就知道。
LinkedHashMap
自身已经实现了顺序存储,默认情况下是按照元素的添加顺序存储,也可以启用按照访问顺序存储,即最近读取的数据放在最前面,最早读取的数据放在最后面,然后它还有一个判断是否删除最老数据的方法,默认是返回false,即不删除数据。大家常见也就是按顺序存储,很少忘了它还可以根据最近未使用的方法。
//LinkedHashMap的一个构造函数,当参数accessOrder为true时,即会按照访问顺序排序,最近访问的放在最前,最早访问的放在后面
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
//LinkedHashMap自带的判断是否删除最老的元素方法,默认返回false,即不删除老数据
//我们要做的就是重写这个方法,当满足一定条件时删除老数据
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
回看我们前面LinkedHashMap
的创建
this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
再举个使用例子
就比较明了了。
BaseMemoryCache
BaseMemoryCache
同样也是实现了MemoryCache
方法,不过它还是一个抽象类。
它是一个内存缓存的基类,实现了内存缓存中常用的方法,只不过它里面提供了一个非强引用的Reference
作为扩展,方便GC的回收,避免OOM.
public abstract class BaseMemoryCache implements MemoryCache {
/** Stores not strong references to objects */
private final Map<String, Reference<Bitmap>> softMap = Collections.synchronizedMap(new HashMap<String, Reference<Bitmap>>());
@Override
public Bitmap get(String key) {
Bitmap result = null;
Reference<Bitmap> reference = softMap.get(key);
if (reference != null) {
result = reference.get();
}
return result;
}
@Override
public boolean put(String key, Bitmap value) {
softMap.put(key, createReference(value));
return true;
}
@Override
public Bitmap remove(String key) {
Reference<Bitmap> bmpRef = softMap.remove(key);
return bmpRef == null ? null : bmpRef.get();
}
/** Creates {@linkplain Reference not strong} reference of value */
protected abstract Reference<Bitmap> createReference(Bitmap value);
}
代码也比较简单,内存持有一个Map<String, Reference<Bitmap>> softMap
来保存非强引用对象,具体的引用类型则看它实现的抽象方法createReference
。
WeakMemoryCache
我们看它的一个子类WeakMemoryCache
则是继承与BaseMemory
,实现createReference
public class WeakMemoryCache extends BaseMemoryCache {
@Override
protected Reference<Bitmap> createReference(Bitmap value) {
return new WeakReference<Bitmap>(value);
}
}
很明显是来保存弱引用对象的。
LimitedMemoryCache
我们看它的另外一个子类LimitedMemoryCache
,但它并没有实现BaseMemoryCache
里的createReference
方法,它也是一个抽象类,在BaseMemoryCache
基础上封装了个抽象方法
protected abstract Bitmap removeNext();
用来处理当缓存容量不足时的情况。
public abstract class LimitedMemoryCache extends BaseMemoryCache {
...
//当前保存的Bitmap,用来统计缓存数
private final List<Bitmap> hardCache = Collections.synchronizedList(new LinkedList<Bitmap>());
...
@Override
public boolean put(String key, Bitmap 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) {
Bitmap 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;
}
@Override
public Bitmap remove(String key) {
Bitmap value = super.get(key);
if (value != null) {
if (hardCache.remove(value)) {
cacheSize.addAndGet(-getSize(value));
}
}
return super.remove(key);
}
...
protected abstract int getSize(Bitmap value);
protected abstract Bitmap removeNext();
}
可以看到在 LimitedMemoryCache
里面又有一个List<Bitmap>
保存的是强引用,而在BaseMemoryCache
里面也有个Map<String, Reference<Bitmap>> softMap
来保存Bitmap,为什么要这样。
这主要是因为在BaseMemoryCache
里面并没有做缓存限制处理,它只是封装实现了基本的Bitmap的put,get。而当面对缓存容量有限的情况下,则需要交给子类去处理。
我们看下这里的put方法,关键在
while (curCacheSize + valueSize > sizeLimit) {
Bitmap removedValue = removeNext();
if (hardCache.remove(removedValue)) {
curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
}
}
当超过容量时,调用抽象方法removeNext
由子类自行实现,之后hardCache移除,但此时并没有调用softMap的移除。
也就是对于List<Bitmap>
来说,当它的缓存容量超过的时候,它会移除第一个对象来缓解容量,但是保存在Map<String, Reference<Bitmap>> softMap
里面的Bitmap并没有被移除。
如果这样下去softMap岂不是会无限大?
这是因为在Map<String, Reference<Bitmap>> softMap
里面保存的Bitmap是弱引用的存在,而在List<Bitmap>
里面保存的是强引用,当内存不足的时候,GC则会先清除softMap里面的对象。
FIFOLimitedMemoryCache
我们看下LimitedMemoryCache
的一个子类FIFOLimitedMemoryCache
,看到FIFO也就是先进先出了。
public class FIFOLimitedMemoryCache extends LimitedMemoryCache {
private final List<Bitmap> queue = Collections.synchronizedList(new LinkedList<Bitmap>());
...
@Override
public boolean put(String key, Bitmap value) {
if (super.put(key, value)) {
queue.add(value);
return true;
} else {
return false;
}
}
@Override
public Bitmap remove(String key) {
Bitmap value = super.get(key);
if (value != null) {
queue.remove(value);
}
return super.remove(key);
}
...
@Override
protected Bitmap removeNext() {
return queue.remove(0);
}
@Override
protected Reference<Bitmap> createReference(Bitmap value) {
return new WeakReference<Bitmap>(value);
}
}
可以看到同样的这里也有个List<Bitmap> queue
来保存记录,而在removeNext
那里,返回的正是队列的第一个元素,符合FIFO。
LRULimitedMemoryCache
再来看一个另外一个子类LRULimitedMemoryCache
也就是最近未使用删除。
public class LRULimitedMemoryCache extends LimitedMemoryCache {
/** Cache providing Least-Recently-Used logic */
private final Map<String, Bitmap> lruCache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(INITIAL_CAPACITY, LOAD_FACTOR, true));
...
@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;
}
@Override
protected Reference<Bitmap> createReference(Bitmap value) {
return new WeakReference<Bitmap>(value);
}
}
可以看到,这里的LRU处理则是使用LinkedHashMap
,在它的构造方法中第三个参数为true
表示使用LRU,之后再removeNext
返回那个Bitmap。
同理其他子类也如下,就不一一列举。
MemoryCache小结
1. 只使用的是强引用缓存
- LruMemoryCache(这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用)
2.使用强引用和弱引用相结合的缓存有
UsingFreqLimitedMemoryCache(如果缓存的图片总量超过限定值,先删除使用频率最小的bitmap)
LRULimitedMemoryCache(这个也是使用的lru算法,和LruMemoryCache不同的是,他缓存的是bitmap的弱引用)
FIFOLimitedMemoryCache(先进先出的缓存策略,当超过设定值,先删除最先加入缓存的bitmap)
LargestLimitedMemoryCache(当超过缓存限定值,先删除最大的bitmap对象)
LimitedAgeMemoryCache(当 bitmap加入缓存中的时间超过我们设定的值,将其删除)
3.只使用弱引用缓存
- WeakMemoryCache(这个类缓存bitmap的总大小没有限制,唯一不足的地方就是不稳定,缓存的图片容易被回收掉)
二.DiskCache硬盘缓存
同样先来看个结构
DiskCache的设计其实和MemoryCache一样,对于基类
DiskCache
,它同样是一个接口
public interface DiskCache {
//返回硬盘缓存的根目录
File getDirectory();
File get(String imageUri);
boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException;
boolean save(String imageUri, Bitmap bitmap) throws IOException;
boolean remove(String imageUri);
void close();
void clear();
}
同样一个个看
LruDiskCache
LruDiskCache
则是直接实现了DiskCache
接口,采用LRU算法来进行缓存处理。
再理解LruDiskCache
前,先理解另一个类DiskLruCache
final class DiskLruCache implements Closeable {
static final String JOURNAL_FILE = "journal";
static final String JOURNAL_FILE_TEMP = "journal.tmp";
static final String JOURNAL_FILE_BACKUP = "journal.bkp";
static final String MAGIC = "libcore.io.DiskLruCache";
...
private final LinkedHashMap<String, Entry> lruEntries =
new LinkedHashMap<String, Entry>(0, 0.75f, true);
...
final ThreadPoolExecutor executorService =
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
...
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize, int maxFileCount)
throws IOException {
...
}
...
public synchronized Snapshot get(String key) throws IOException {
...
}
...
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
...
}
/** A snapshot of the values for an entry. */
public final class Snapshot implements Closeable {
private final String key;
private final long sequenceNumber;
private File[] files;
private final InputStream[] ins;
private final long[] lengths;
...
}
...
public final class Editor {
private final Entry entry;
private final boolean[] written;
private boolean hasErrors;
private boolean committed;
...
}
...
private final class Entry {
private final String key;
private final long[] lengths;
private boolean readable;
private Editor currentEditor;
private long sequenceNumber;
...
}
这个DiskLruCache
比较长也比较复杂,它是LruDiskCache
的一个文件工具类。这里的缓存数据存储在文件系统上的一个目录。
同时也注意到这里的一个成员变量
private final LinkedHashMap<String, Entry> lruEntries =new LinkedHashMap<String, Entry>(0, 0.75f, true);
可以知道这是用来处理LRU的。
同时这里的value则是Entry
,Entry
则是封装了当前文件的编辑情况Ediotr
以及key
。
而这里Editor
封装了文件的写入情况OutputStream
,Snapshot
封装了文件的读取情况InputStream
。
回头看回LruDiskCache
public class LruDiskCache implements DiskCache {
protected DiskLruCache cache;
private File reserveCacheDir;
protected final FileNameGenerator fileNameGenerator;
...
public LruDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator, long cacheMaxSize,
int cacheMaxFileCount) throws IOException {
...
this.reserveCacheDir = reserveCacheDir;
this.fileNameGenerator = fileNameGenerator;
initCache(cacheDir, reserveCacheDir, cacheMaxSize, cacheMaxFileCount);
}
private void initCache(File cacheDir, File reserveCacheDir, long cacheMaxSize, int cacheMaxFileCount)
...
cache = DiskLruCache.open(cacheDir, 1, 1, cacheMaxSize, cacheMaxFileCount);
...
}
@Override
public File get(String imageUri) {
DiskLruCache.Snapshot snapshot = null;
try {
snapshot = cache.get(getKey(imageUri));
return snapshot == null ? null : snapshot.getFile(0);
}
...
}
@Override
public boolean save(String imageUri, Bitmap bitmap) throws IOException {
DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
...
OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
boolean savedSuccessfully = false;
try {
savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
}
...
return savedSuccessfully;
}
首先LruDiskCache
内部成员变量带有DiskLruCache
还有文件的保存目录等,在它的构造方法中调用DiskLruCache.open
方法创建了DiskLruCache
对象,而在它的open方法里,则根据文件的目录情况创建了对应的文件系统。
再看它的save方法,先调用getKey
方法将uri转换为对应的key,而在cache,edit中
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
...
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) {
return null; // Snapshot is stale.
}
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
} else if (entry.currentEditor != null) {
return null; // Another edit is in progress.
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
...
return editor;
}
则是根据指定的key先判断缓存文件中有没有相应的key,如果没有则创建一个Entry
对象持有它,之后保存在lruEntries
之后,创建一个当前Entry
的编辑对象Editor
,以便之后写入到文件中。
s之后调用了
OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
在editor.newOutputStream
则是根据当前目录和key创建出一个文件,之后打开这个文件的一个输出流情况,获取到之后就进行Bitmap的写入。
同理,看下LruDiskCache
的get方法
@Override
public File get(String imageUri) {
DiskLruCache.Snapshot snapshot = null;
try {
snapshot = cache.get(getKey(imageUri));
return snapshot == null ? null : snapshot.getFile(0);
}
...
}
调用了cache,get
public synchronized Snapshot get(String key) throws IOException {
。。。
Entry entry = lruEntries.get(key);
...
File[] files = new File[valueCount];
InputStream[] ins = new InputStream[valueCount];
try {
File file;
for (int i = 0; i < valueCount; i++) {
file = entry.getCleanFile(i);
files[i] = file;
ins[i] = new FileInputStream(file);
}
}
...
return new Snapshot(key, entry.sequenceNumber, files, ins, entry.lengths);
}
在get方法中,先根据key拿到对应的Entry
,再拿到对应的文件打开输入流,之后传入到Snapshot
,
而在snapshot.getFile
中
/** Returns file with the value for {@code index}. */
public File getFile(int index) {
return files[index];
}
返回的则是对应的文件。
BaseDiskCache
BaseDiskCache
同样也是直接实现了DiskCache
方法,实现的方法也比较简单
public abstract class BaseDiskCache implements DiskCache {
...
protected final File cacheDir;
protected final File reserveCacheDir;
protected final FileNameGenerator fileNameGenerator;
public BaseDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator) {
...
this.cacheDir = cacheDir;
this.reserveCacheDir = reserveCacheDir;
this.fileNameGenerator = fileNameGenerator;
}
@Override
public boolean save(String imageUri, Bitmap bitmap) throws IOException {
File imageFile = getFile(imageUri);
File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
boolean savedSuccessfully = false;
try {
savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
} finally {
IoUtils.closeSilently(os);
if (savedSuccessfully && !tmpFile.renameTo(imageFile)) {
savedSuccessfully = false;
}
if (!savedSuccessfully) {
tmpFile.delete();
}
}
bitmap.recycle();
return savedSuccessfully;
}
@Override
public File get(String imageUri) {
return getFile(imageUri);
}
protected File getFile(String imageUri) {
String fileName = fileNameGenerator.generate(imageUri);
File dir = cacheDir;
if (!cacheDir.exists() && !cacheDir.mkdirs()) {
if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {
dir = reserveCacheDir;
}
}
return new File(dir, fileName);
}
比较简单,根据对应的文件去打开获取。它的两个子类LimitedAgeDiskCache
和UnlimitedDiskCache
也都不一一扩展开了。