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
也都不一一擴展開了。