阿裡P7移動網際網路架構師進階視訊(每日更新中)免費學習請點選: https://space.bilibili.com/474380680 本篇文章将通過Universal-Image-Loader解析來闡述圖檔加載架構選型:
一、[Universal-Image-Loader解析基本介紹與使用]
基本介紹
相信大家平時做Android應用的時候,多少會接觸到異步加載圖檔,或者加載大量圖檔的問題,而加載圖檔我們常常會遇到許多的問題,比如說圖檔的錯亂,OOM等問題,對于新手來說,這些問題解決起來會比較吃力,是以就有很多的開源圖檔加載架構應運而生,比較著名的就是Universal-Image-Loader,相信很多朋友都聽過或者使用過這個強大的圖檔加載架構,今天這篇文章就是對這個架構的基本介紹以及使用,主要是幫助那些沒有使用過這個架構的朋友們。該項目存在于Github上面
Android-Universal-Image-Loader,我們可以先看看這個開源庫存在哪些特征
- 多線程下載下傳圖檔,圖檔可以來源于網絡,檔案系統,項目檔案夾assets中以及drawable中等
- 支援随意的配置ImageLoader,例如線程池,圖檔下載下傳器,記憶體緩存政策,硬碟緩存政策,圖檔顯示選項以及其他的一些配置
- 支援圖檔的記憶體緩存,檔案系統緩存或者SD卡緩存
- 支援圖檔下載下傳過程的監聽
- 根據控件(ImageView)的大小對Bitmap進行裁剪,減少Bitmap占用過多的記憶體
- 較好的控制圖檔的加載過程,例如暫停圖檔加載,重新開始加載圖檔,一般使用在 ListView,GridView中,滑動過程中暫停加載圖檔,停止滑動的時候去加載圖檔
- 提供在較慢的網絡下對圖檔進行加載
當然上面列舉的特性可能不全,要想了解一些其他的特性隻能通過我們的使用慢慢去發現了
ImageLoaderConfiguration
圖檔加載器ImageLoader的配置參數,使用Builder模式。
常用的配置屬性有
//通過StorageUtils擷取内置的記憶體目錄/data/data/.../cache
File cacheDir = StorageUtils.getCacheDirectory(context);
ImageLoaderConfiguration config = new ImageLoaderConfiguration
.Builder(getApplicationContext())
.memoryCacheExtraOptions(480, 800) //即儲存的每個緩存檔案的最大長寬
.threadPoolSize(3) //線程池内加載的數量
.threadPriority(Thread.NORM_PRIORITY - 2)
//解釋:當同一個Uri擷取不同大小的圖檔,緩存到記憶體時,隻緩存一個。預設會緩存多個不同的大小的相同圖檔
.denyCacheImageMultipleSizesInMemory() //拒絕緩存多個圖檔。
.memoryCache(new WeakMemoryCache()) //緩存政策你可以通過自己的記憶體緩存實作 ,這裡用弱引用,缺點是太容易被回收了,不是很好!
.memoryCacheSize(2 * 1024 * 1024) //設定記憶體緩存的大小
.diskCacheSize(50 * 1024 * 1024) //設定磁盤緩存大小 50M
.diskCacheFileNameGenerator(new Md5FileNameGenerator()) //将儲存的時候的URI名稱用MD5 加密
.tasksProcessingOrder(QueueProcessingType.LIFO) //設定圖檔下載下傳和顯示的工作隊列排序
.diskCacheFileCount(100) //緩存的檔案數量
.diskCache(new UnlimitedDiskCache(cacheDir)) //自定義緩存路徑
.defaultDisplayImageOptions(defaultOptions) //顯示圖檔的參數,預設:DisplayImageOptions.createSimple()
.imageDownloader(new BaseImageDownloader(this, 5 * 1000, 30 * 1000)) // connectTimeout (5 s), readTimeout (30 s)逾時時間
.writeDebugLogs() //打開調試日志
.build();//開始建構
//配置使用
ImageLoader.getInstance().init(configuration);
可以設定記憶體緩存,硬碟緩存的相關參數等。
設定完相關的參數後就可進行圖檔加載顯示
圖檔加載
ImageLader提供了幾個圖檔加載的方法,主要是這幾個displayImage(), loadImage(),loadImageSync(),loadImageSync()方法是同步的,android4.0有個特性,網絡操作不能在主線程,是以loadImageSync()方法我們就不去使用
loadimage()加載圖檔
我們先使用ImageLoader的loadImage()方法來加載網絡圖檔
final ImageView mImageView = (ImageView) findViewById(R.id.image);
String imageUrl = "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s1024/A%252520Photographer.jpg";
ImageLoader.getInstance().loadImage(imageUrl, new ImageLoadingListener() {
@Override
public void onLoadingStarted(String imageUri, View view) {
}
@Override
public void onLoadingFailed(String imageUri, View view,
FailReason failReason) {
}
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
mImageView.setImageBitmap(loadedImage);
}
@Override
public void onLoadingCancelled(String imageUri, View view) {
}
});
傳入圖檔的url和ImageLoaderListener, 在回調方法onLoadingComplete()中将loadedImage設定到ImageView上面就行了,如果你覺得傳入ImageLoaderListener太複雜了,我們可以使用SimpleImageLoadingListener類,該類提供了ImageLoaderListener接口方法的空實作,使用的是預設擴充卡模式
final ImageView mImageView = (ImageView) findViewById(R.id.image);
String imageUrl = "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s1024/A%252520Photographer.jpg";
ImageLoader.getInstance().loadImage(imageUrl, new SimpleImageLoadingListener(){
@Override
public void onLoadingComplete(String imageUri, View view,
Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
mImageView.setImageBitmap(loadedImage);
}
});
如果我們要指定圖檔的大小該怎麼辦呢,這也好辦,初始化一個ImageSize對象,指定圖檔的寬和高,代碼如下
final ImageView mImageView = (ImageView) findViewById(R.id.image);
String imageUrl = "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s1024/A%252520Photographer.jpg";
ImageSize mImageSize = new ImageSize(100, 100);
ImageLoader.getInstance().loadImage(imageUrl, mImageSize, new SimpleImageLoadingListener(){
@Override
public void onLoadingComplete(String imageUri, View view,
Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
mImageView.setImageBitmap(loadedImage);
}
});
上面隻是很簡單的使用ImageLoader來加載網絡圖檔,在實際的開發中,我們并不會這麼使用,那我們平常會怎麼使用呢?我們會用到DisplayImageOptions,他可以配置一些圖檔顯示的選項,比如圖檔在加載中ImageView顯示的圖檔,是否需要使用記憶體緩存,是否需要使用檔案緩存等等
DisplayImageOptions
可以配置一些圖檔顯示的選項,比如圖檔在加載中ImageView顯示的圖檔,是否需要使用記憶體緩存,是否需要使用檔案緩存等等,可供我們選擇的配置如下
DisplayImageOptions options = new DisplayImageOptions.Builder()
.showImageOnLoading(R.drawable.ic_stub) // resource or drawable
.showImageForEmptyUri(R.drawable.ic_empty) // resource or drawable
.showImageOnFail(R.drawable.ic_error) // resource or drawable
.resetViewBeforeLoading(false) // default
.delayBeforeLoading(1000)
.cacheInMemory(false) // default
.cacheOnDisk(false) // default
.preProcessor(...)
.postProcessor(...)
.extraForDownloader(...)
.considerExifParams(false) // default
.imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2) // default
.bitmapConfig(Bitmap.Config.ARGB_8888) // default
.decodingOptions(...)
.displayer(new SimpleBitmapDisplayer()) // default
.handler(new Handler()) // default
.build();
大家就可以根據實際情況去設定。
displayImage()加載圖檔
接下來我們就來看看網絡圖檔加載的另一個方法displayImage(),代碼如下
String imageUrl = "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s1024/A%252520Photographer.jpg";
//顯示圖檔的配置
DisplayImageOptions options = new DisplayImageOptions.Builder()
.showImageOnLoading(R.drawable.ic_stub)
.showImageOnFail(R.drawable.ic_error)
.cacheInMemory(true)
.cacheOnDisk(true)
.bitmapConfig(Bitmap.Config.RGB_565)
.build();
ImageLoader.getInstance().displayImage(imageUrl, mImageView, options);
可以看到這裡是直接傳遞了ImageView進行設定顯示,并不需要監聽後設定,這樣更為簡便,這也是
displayImage
和
loadImage
的差別。
加載其他來源的圖檔
使用Universal-Image-Loader架構不僅可以加載網絡圖檔,還可以加載sd卡中的圖檔,Content provider等,使用也很簡單,隻是将圖檔的url稍加的改變下就行了,下面是加載檔案系統的圖檔
我們隻需要給每個圖檔來源的地方加上Scheme包裹起來(Content provider除外),然後當做圖檔的url傳遞到imageLoader中,Universal-Image-Loader架構會根據不同的Scheme擷取到輸入流
//圖檔來源于檔案
String imagePath = "/mnt/sdcard/image.png";
String imageUrl = Scheme.FILE.wrap(imagePath);
//相當于file:/mnt/sdcard/image.png
//圖檔來源于Content provider
String contentprividerUrl = "content://media/external/audio/albumart/13";
//圖檔來源于assets
String assetsUrl = Scheme.ASSETS.wrap("image.png");
//圖檔來源于
String drawableUrl = Scheme.DRAWABLE.wrap("R.drawable.image");
擷取到對應URL後就可以調用display/loadImage方法進行顯示。
GirdView,ListView加載圖檔
相信大部分人都是使用GridView,ListView來顯示大量的圖檔,而當我們快速滑動GridView,ListView,我們希望能停止圖檔的加載,而在GridView,ListView停止滑動的時候加載目前界面的圖檔,這個架構當然也提供這個功能,使用起來也很簡單,它提供了PauseOnScrollListener這個類來控制ListView,GridView滑動過程中停止去加載圖檔,該類使用的是代理模式
listView.setOnScrollListener(new PauseOnScrollListener(imageLoader, pauseOnScroll, pauseOnFling));
gridView.setOnScrollListener(new PauseOnScrollListener(imageLoader, pauseOnScroll, pauseOnFling));
第一個參數就是我們的圖檔加載對象ImageLoader,
第二個是控制是否在滑動過程中暫停加載圖檔,如果需要暫停傳true就行了,
第三個參數控制猛的滑動界面的時候圖檔是否加載
OutOfMemoryError
雖然這個架構有很好的緩存機制,有效的避免了OOM的産生,一般的情況下産生OOM的機率比較小,但是并不能保證OutOfMemoryError永遠不發生,這個架構對于OutOfMemoryError做了簡單的catch,保證我們的程式遇到OOM而不被crash掉,但是如果我們使用該架構經常發生OOM,我們應該怎麼去改善呢?
- 減少線程池中線程的個數,在ImageLoaderConfiguration中的(.threadPoolSize)中配置,推薦配置1-5
- 在DisplayImageOptions選項中配置bitmapConfig為Bitmap.Config.RGB_565,因為預設是ARGB_8888, 使用RGB_565會比使用ARGB_8888少消耗2倍的記憶體
- 在ImageLoaderConfiguration中配置圖檔的記憶體緩存為memoryCache(new WeakMemoryCache()) 或者不使用記憶體緩存
- 在DisplayImageOptions選項中設定.imageScaleType(ImageScaleType.IN_SAMPLE_INT)或者imageScaleType(ImageScaleType.EXACTLY)
二、[Universal-Image-Loader解析内部緩存原理]
對于我們所知道的緩存,常用的是記憶體緩存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
也都不一一擴充開了。
三、Universal-Image-Loader解析之源代碼解析
當我們配置好
ImageConfiguration
ImageLoader
後,我們就會開始調用
ImageLoader.getInstance().loadImage(...);
ImageLoader.getInstance().displayImage(...);
這兩個方法其中一個來顯示圖檔。
先看
loadImage
public void loadImage(String uri, ImageSize targetImageSize, DisplayImageOptions options,
ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
checkConfiguration();
if (targetImageSize == null) {
targetImageSize = configuration.getMaxImageSize();
}
if (options == null) {
options = configuration.defaultDisplayImageOptions;
}
NonViewAware imageAware = new NonViewAware(uri, targetImageSize, ViewScaleType.CROP);
displayImage(uri, imageAware, options, listener, progressListener);
}
首先調用了
checkConfiguration
用來判斷是否有初始化
ImageLoaderConfiguration
如果有設定ImageView的大小,則設定,沒則預設Configuration的大小。
如果沒有設定
DisplayImageOptions
,則設定上一個預設的options
之後建立了個
NonViewAware
,再調用
displayImage
也就是說,
loadImage
最終還是調用到了
displayImage
ImageAware
這裡的
NonViewAware
實作了
ImageAware
接口。先來看個結構圖
ImageAware
是一個接口,内部提供了一系列操作圖檔的一些方法。
對于
NonViewAware
來說,它内部隻是簡單的儲存圖檔一些必要的資料,比如圖檔大小尺寸,URI,ScaleType這些。主要封裝成
ImageAware
來給
displayImage
調用。
看下
displayImage
的使用
public void displayImage(String uri, ImageView imageView) {
displayImage(uri, new ImageViewAware(imageView), null, null, null);
}
這裡把ImageView封裝成
ImageViewAware
再去調用
displayImage
這個就跟
loadImage
一樣。
ImageViewAware
繼承與
ViewAware
,
ViewAware
則實作了
ImageAware
接口。
與
NonViewAware
不同的是
ViewAware
内部持有一個
Reference<View> viewRef
的成員變量,它是用來儲存目前
ImageView
的一個弱引用,以便之後來直接設定顯示圖檔。
ViewAware
很多方法都是依賴于這個View
@Override
public boolean setImageDrawable(Drawable drawable) {
if (Looper.myLooper() == Looper.getMainLooper()) {
View view = viewRef.get();
if (view != null) {
setImageDrawableInto(drawable, view);
return true;
}
} else {
L.w(WARN_CANT_SET_DRAWABLE);
}
return false;
}
之後就可以在
ImageViewAware
中設定顯示。
好了回過頭看他們最終調用的方法。
這個方法有點長,我們拆分成一部分一部分來看
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
checkConfiguration();
if (imageAware == null) {
throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
}
if (listener == null) {
listener = defaultListener;
}
if (options == null) {
options = configuration.defaultDisplayImageOptions;
}
if (TextUtils.isEmpty(uri)) {
engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
if (options.shouldShowImageForEmptyUri()) {
imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
} else {
imageAware.setImageDrawable(null);
}
listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
return;
}
...//下一部分看
}
首先先檢查是否有初始化設定
ImageLoaderConfiguration
沒則抛出異常,沒設定listener和DisplayImageOptions則設定一個預設值。
之後調用
TextUtils.isEmpty(uri)
判斷是否目前的uri為空,則調用
engine.cancelDisplayTaskFor(imageAware);
之後則用listener通知開始和結束,也比較好了解,主要是這個engine。
這個engine就是
ImageLoaderEngine
,主要用來負責顯示加載圖檔的一個類。
ImageLoaderEngine
中存在一個HashMap,用來記錄正在加載的任務,加載圖檔的時候會将ImageView的id和圖檔的url加上尺寸加入到HashMap中,加載完成之後會将其移除。
接着看下面
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
...//前一部分
if (targetSize == null) {
targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
}
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp != null && !bmp.isRecycled()) {
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
if (options.shouldPostProcess()) {
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
} else {
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
}
...//下一部分
}
當URI不為空的時候來加載顯示。首先根據uri擷取對應uri對應唯一的一個Key,之後調用
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
來記錄目前加載的任務,開啟listener的start回調,接着調用
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
來擷取記憶體緩存中的圖檔,這裡預設的記憶體緩存是
LruMemoryCache
,前篇文章有分析到。
如果緩存中存在相應的Bitmap的話,進入到if裡面
我們如果在DisplayImageOptions中設定了postProcessor就進入true邏輯,不過預設postProcessor是為null的,BitmapProcessor接口主要是對Bitmap進行處理,這個架構并沒有給出相對應的實作,如果我們有自己的需求的時候可以自己實作BitmapProcessor接口(比如将圖檔設定成圓形的).
然後到了27行
将Bitmap設定到ImageView上面,這裡我們可以在DisplayImageOptions中配置顯示需求displayer,預設使用的是SimpleBitmapDisplayer,直接将Bitmap設定到ImageView上面,我們可以配置其他的顯示邏輯, 他這裡提供了FadeInBitmapDisplayer(透明度從0-1)RoundedBitmapDisplayer(4個角是圓弧)等, 然後回調到ImageLoadingListener接口。
我們知道
loadImage
displayImage
的差別在于
loadImage
依靠傳回的Bitmap進行設定顯示,而
displayImage
則是直接顯示。而
loadImage
最終也是調用了
displayImage
,原因就在于這個display和imageAware
public final class SimpleBitmapDisplayer implements BitmapDisplayer {
@Override
public void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom) {
imageAware.setImageBitmap(bitmap);
}
}
loadImage
的
ImageAware
是
NonImageAware
并沒有處理
setImageBitmap
的方法,而
displayImage
ImageViewAware
則有處理顯示。
好,繼續前面,當從記憶體緩存擷取到的Bitmap為空的情況下
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
...//前兩部分
//如果Bitmap為空
} else {
if (options.shouldShowImageOnLoading()) {
imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
} else if (options.isResetViewBeforeLoading()) {
imageAware.setImageDrawable(null);
}
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
}
}
如果需要設定顯示加載中的圖檔,則進行設定顯示。
ImageLoadingInfo
則是一個加載顯示圖檔任務資訊的一個類。
之後根據它建立了一個
LoadAndDisplayImageTask
類,它實作了
Runnable
如果配置了isSyncLoading為true, 直接執行LoadAndDisplayImageTask的run方法,表示同步,預設是false,将LoadAndDisplayImageTask送出給線程池對象
接下來我們就看LoadAndDisplayImageTask的run(), 這個類還是蠻複雜的,我們還是一段一段的分析。
@Override
public void run() {
if (waitIfPaused()) return;
if (delayIfNeed()) return;
...
}
如果waitIfPaused(), delayIfNeed()傳回true的話,直接從run()方法中傳回了,不執行下面的邏輯, 接下來我們先看看waitIfPaused()
private boolean waitIfPaused() {
AtomicBoolean pause = engine.getPause();
if (pause.get()) {
synchronized (engine.getPauseLock()) {
if (pause.get()) {
L.d(LOG_WAITING_FOR_RESUME, memoryCacheKey);
try {
engine.getPauseLock().wait();
} catch (InterruptedException e) {
L.e(LOG_TASK_INTERRUPTED, memoryCacheKey);
return true;
}
L.d(LOG_RESUME_AFTER_PAUSE, memoryCacheKey);
}
}
}
return isTaskNotActual();
}
這個方法是幹嘛用呢,主要是我們在使用ListView,GridView去加載圖檔的時候,有時候為了滑動更加的流暢,我們會選擇手指在滑動或者猛地一滑動的時候不去加載圖檔,是以才提出了這麼一個方法,那麼要怎麼用呢? 這裡用到了PauseOnScrollListener這個類,使用很簡單ListView.setOnScrollListener(new PauseOnScrollListener(pauseOnScroll, pauseOnFling )), pauseOnScroll控制我們緩慢滑動ListView,GridView是否停止加載圖檔,pauseOnFling 控制猛的滑動ListView,GridView是否停止加載圖檔。
我們可以看下這個
PauseOnScrollListener
的處理
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
case OnScrollListener.SCROLL_STATE_IDLE:
imageLoader.resume();
break;
case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
if (pauseOnScroll) {
imageLoader.pause();
}
break;
case OnScrollListener.SCROLL_STATE_FLING:
if (pauseOnFling) {
imageLoader.pause();
}
break;
}
if (externalListener != null) {
externalListener.onScrollStateChanged(view, scrollState);
}
}
滑動停止的話會調用到
imageLoader.pause
public void pause() {
engine.pause();
}
...
void pause() {
paused.set(true);
}
這裡的pause是
private final AtomicBoolean paused = new AtomicBoolean(false);
是以調用
pause.get
則會傳回true。
除此之外,這個方法的傳回值由isTaskNotActual()決定,我們接着看看isTaskNotActual()的源碼
private boolean isTaskNotActual() {
return isViewCollected() || isViewReused();
}
isViewCollected()是判斷我們ImageView是否被垃圾回收器回收了,如果回收了,LoadAndDisplayImageTask方法的run()就直接傳回了,isViewReused()判斷該ImageView是否被重用,被重用run()方法也直接傳回,為什麼要用isViewReused()方法呢?主要是ListView,GridView我們會複用item對象,假如我們先去加載ListView,GridView第一頁的圖檔的時候,第一頁圖檔還沒有全部加載完我們就快速的滾動,isViewReused()方法就會避免這些不可見的item去加載圖檔,而直接加載目前界面的圖檔。
回頭繼續看run方法
@Override
public void run() {
...
ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
if (loadFromUriLock.isLocked()) {
L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
}
loadFromUriLock.lock();
Bitmap bmp;
try {
checkTaskNotActual();
bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp == null || bmp.isRecycled()) {
bmp = tryLoadBitmap();
if (bmp == null) return; // listener callback already was fired
checkTaskNotActual();
checkTaskInterrupted();
if (options.shouldPreProcess()) {
L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
bmp = options.getPreProcessor().process(bmp);
if (bmp == null) {
L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
}
}
if (bmp != null && options.isCacheInMemory()) {
L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey);
configuration.memoryCache.put(memoryCacheKey, bmp);
}
} else {
loadedFrom = LoadedFrom.MEMORY_CACHE;
L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey);
}
if (bmp != null && options.shouldPostProcess()) {
L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey);
bmp = options.getPostProcessor().process(bmp);
if (bmp == null) {
L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey);
}
}
checkTaskNotActual();
checkTaskInterrupted();
} catch (TaskCancelledException e) {
fireCancelEvent();
return;
} finally {
loadFromUriLock.unlock();
}
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
runTask(displayBitmapTask, syncLoading, handler, engine);
}
第4行代碼有一個loadFromUriLock,這個是一個鎖,擷取鎖的方法在ImageLoaderEngine類的getLockForUri()方法中
ReentrantLock getLockForUri(String uri) {
ReentrantLock lock = uriLocks.get(uri);
if (lock == null) {
lock = new ReentrantLock();
uriLocks.put(uri, lock);
}
return lock;
}
從上面可以看出,這個鎖對象與圖檔的url是互相對應的,為什麼要這麼做?也行你還有點不了解,不知道大家有沒有考慮過一個場景,假如在一個ListView中,某個item正在擷取圖檔的過程中,而此時我們将這個item滾出界面之後又将其滾進來,滾進來之後如果沒有加鎖,該item又會去加載一次圖檔,假設在很短的時間内滾動很頻繁,那麼就會出現多次去網絡上面請求圖檔,是以這裡根據圖檔的Url去對應一個ReentrantLock對象,讓具有相同Url的請求就會在第10行等待,等到這次圖檔加載完成之後,ReentrantLock就被釋放,剛剛那些相同Url的請求就會繼續執行第10行下面的代碼。
之後來到第13行,先調用
checkTaskNotActual
判斷目前View是否被GC回收使用,是則抛出異常。
接着15行,它們會先從記憶體緩存中擷取一遍,如果記憶體緩存中沒有在去執行下面的邏輯,是以ReentrantLock的作用就是避免這種情況下重複的去從網絡上面請求圖檔。
17行的方法
tryLoadBitmap()
,這個方法确實也有點長,我先告訴大家,這裡面的邏輯是先從檔案緩存中擷取有沒有Bitmap對象,如果沒有在去從網絡中擷取,然後将bitmap儲存在檔案系統中,我們還是具體分析下
private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
try {
File imageFile = configuration.diskCache.get(uri);
if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
loadedFrom = LoadedFrom.DISC_CACHE;
checkTaskNotActual();
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
}
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);
loadedFrom = LoadedFrom.NETWORK;
String imageUriForDecoding = uri;
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
imageFile = configuration.diskCache.get(uri);
if (imageFile != null) {
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
}
checkTaskNotActual();
bitmap = decodeImage(imageUriForDecoding);
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
fireFailEvent(FailType.DECODING_ERROR, null);
}
}
}
...
return bitmap;
}
首先在第4行會去磁盤緩存中去擷取圖檔,如果圖檔已經儲存在磁盤了,則直接擷取對應的File路徑,調用
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
進行解析。
如果在磁盤中沒有的話,則到了12行,開始進行網絡下載下傳擷取。
在17行會去調用
isCacheOnDisk
判斷是否要保持在磁盤中,如果預設false,如果是則調用
tryCacheImageOnDisk
來下載下傳圖檔并且保持在磁盤
private boolean tryCacheImageOnDisk() throws TaskCancelledException {
L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);
boolean loaded;
try {
loaded = downloadImage();
...
} ...
return loaded;
}
downloadImage
進行下載下傳圖檔
private boolean downloadImage() throws IOException {
InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader());
if (is == null) {
L.e(ERROR_NO_IMAGE_STREAM, memoryCacheKey);
return false;
} else {
try {
return configuration.diskCache.save(uri, is, this);
} finally {
IoUtils.closeSilently(is);
}
}
}
可以看到這裡調用了
getDownloader().getStream
來下載下傳,這裡先不擴充,在後面會說到
下載下傳之後則儲存在磁盤中。
回來前面
String imageUriForDecoding = uri;
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
imageFile = configuration.diskCache.get(uri);
if (imageFile != null) {
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
}
checkTaskNotActual();
bitmap = decodeImage(imageUriForDecoding);
這裡有個String變量imageUriForDecoding,初始值是uri,如果有設定磁盤緩存的話,則會調用
tryCacheImageOnDisk
來下載下傳并且保持圖檔,此時的imageUriForDecoding則是檔案File的路徑。
如果沒有設定磁盤緩存的話,則imageUriForDecoding還是uri。
關鍵則是在
decodeImage
,它能根據對應的uri來加載圖檔。
private Bitmap decodeImage(String imageUri) throws IOException {
ViewScaleType viewScaleType = imageAware.getScaleType();
ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
getDownloader(), options);
return decoder.decode(decodingInfo);
}
把傳遞進來的imageUri(可能是檔案的uri,也可能是圖檔的uri)封裝到
ImageDecodingInfo
這裡的decoder是
ImageDecode
,它的預設實作類是
BaseImageDecode
@Override
public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
Bitmap decodedBitmap;
ImageFileInfo imageInfo;
InputStream imageStream = getImageStream(decodingInfo);
...
}
通過
getImageStream
來擷取輸入流
protected InputStream getImageStream(ImageDecodingInfo decodingInfo) throws IOException {
return decodingInfo.getDownloader().getStream(decodingInfo.getImageUri(), decodingInfo.getExtraForDownloader());
}
這裡的Downloader預設實作類是
BaseImageDownloader
@Override
public InputStream getStream(String imageUri, Object extra) throws IOException {
switch (Scheme.ofUri(imageUri)) {
case HTTP:
case HTTPS:
return getStreamFromNetwork(imageUri, extra);
case FILE:
return getStreamFromFile(imageUri, extra);
case CONTENT:
return getStreamFromContent(imageUri, extra);
case ASSETS:
return getStreamFromAssets(imageUri, extra);
case DRAWABLE:
return getStreamFromDrawable(imageUri, extra);
case UNKNOWN:
default:
return getStreamFromOtherSource(imageUri, extra);
}
}
可以看到,在這裡,已經做了多種情況的讀取判斷。第一篇文章就有介紹到UIL可以根據不同的uri來解析圖檔,其原理就是在這裡。
而前面通過
tryCacheImageOnDisk
來下載下傳圖檔也是根據這個。這裡就不一一擴充開。
這裡的網絡下載下傳圖檔内部則是使用
HttpUrlConnection
來下載下傳的。
回到最前面
LoadAndDisplayImageTask
的run方法後面,當我們擷取到Bitmap後,到了
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
runTask(displayBitmapTask, syncLoading, handler, engine);
這兩個代碼就是一個顯示任務
直接看DisplayBitmapTask類的run()方法
@Override
public void run() {
if (imageAware.isCollected()) {
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else if (isViewWasReused()) {
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else {
L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey);
displayer.display(bitmap, imageAware, loadedFrom);
engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
}
}
假如ImageView被回收了或者被重用了,回調給ImageLoadingListener接口,否則就調用BitmapDisplayer去顯示Bitmap。到這裡Bitmap已經顯示加載完成,調用engine移除圖檔顯示任務。
當然在最前面那裡
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
...
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
}
}
如果此時的顯示加載是異步的話,則交由engine的
Executor
線程池去處理,最終也是調用了
LoadAndDisplayImageTask
的run方法去加載顯示。
到這裡Universal-Image-Loader的分析也算完了,從基本使用到記憶體模型在加載顯示,可以看到UIL這個開源架構十分的靈活,比如建造者模式,裝飾模式,代理模式,政策模式等等,這樣友善我們去擴充,實作我們想要的功能,當然,也帶給我們更多的想象空間。
原文連結:
https://www.jianshu.com/p/cff58eddb4ae