Universal-Image-Loader解析系列
Universal-Image-Loader解析(一)基本介紹與使用
Universal-Image-Loader解析(二)內(nèi)部緩存原理
Universal-Image-Loader解析(三)源代碼解析
前兩篇文章主要跟大家介紹了UIL的基本使用以及它的緩存設計,本篇文章主要講解它的源代碼
當我們配置好ImageConfiguration和ImageLoader后,我們就會開始調(diào)用
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);
}
首先調(diào)用了checkConfiguration用來判斷是否有初始化ImageLoaderConfiguration
如果有設置ImageView的大小,則設置,沒則默認Configuration的大小。
如果沒有設置DisplayImageOptions,則設置上一個默認的options
之后創(chuàng)建了個NonViewAware,再調(diào)用displayImage。
也就是說,loadImage最終還是調(diào)用到了displayImage。
ImageAware
這里的NonViewAware實現(xiàn)了ImageAware接口。先來看個結構圖

ImageAware是一個接口,內(nèi)部提供了一系列操作圖片的一些方法。
對于NonViewAware來說,它內(nèi)部只是簡單的保存圖片一些必要的數(shù)據(jù),比如圖片大小尺寸,URI,ScaleType這些。主要封裝成ImageAware來給displayImage調(diào)用。
看下displayImage的使用
public void displayImage(String uri, ImageView imageView) {
displayImage(uri, new ImageViewAware(imageView), null, null, null);
}
這里把ImageView封裝成ImageViewAware再去調(diào)用displayImage這個就跟loadImage一樣。
而這里ImageViewAware繼承與ViewAware,ViewAware則實現(xiàn)了ImageAware接口。
與NonViewAware不同的是ViewAware內(nèi)部持有一個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中設置顯示。
好了回過頭看他們最終調(diào)用的方法。
這個方法有點長,我們拆分成一部分一部分來看
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則設置一個默認值。
之后調(diào)用TextUtils.isEmpty(uri)判斷是否當前的uri為空,則調(diào)用
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不為空的時候來加載顯示。首先根據(jù)uri獲取對應uri對應唯一的一個Key,之后調(diào)用engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);來記錄當前加載的任務,開啟listener的start回調(diào),接著調(diào)用Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);來獲取內(nèi)存緩存中的圖片,這里默認的內(nèi)存緩存是LruMemoryCache,前篇文章有分析到。
如果緩存中存在相應的Bitmap的話,進入到if里面
我們?nèi)绻贒isplayImageOptions中設置了postProcessor就進入true邏輯,不過默認postProcessor是為null的,BitmapProcessor接口主要是對Bitmap進行處理,這個框架并沒有給出相對應的實現(xiàn),如果我們有自己的需求的時候可以自己實現(xiàn)BitmapProcessor接口(比如將圖片設置成圓形的).
然后到了27行
將Bitmap設置到ImageView上面,這里我們可以在DisplayImageOptions中配置顯示需求displayer,默認使用的是SimpleBitmapDisplayer,直接將Bitmap設置到ImageView上面,我們可以配置其他的顯示邏輯, 他這里提供了FadeInBitmapDisplayer(透明度從0-1)RoundedBitmapDisplayer(4個角是圓弧)等, 然后回調(diào)到ImageLoadingListener接口。
我們知道loadImage和displayImage的區(qū)別在于loadImage依靠返回的Bitmap進行設置顯示,而displayImage則是直接顯示。而loadImage最終也是調(diào)用了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則有處理顯示。
好,繼續(xù)前面,當從內(nèi)存緩存獲取到的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則是一個加載顯示圖片任務信息的一個類。
之后根據(jù)它創(chuàng)建了一個LoadAndDisplayImageTask類,它實現(xiàn)了Runnable。
如果配置了isSyncLoading為true, 直接執(zhí)行LoadAndDisplayImageTask的run方法,表示同步,默認是false,將LoadAndDisplayImageTask提交給線程池對象
接下來我們就看LoadAndDisplayImageTask的run(), 這個類還是蠻復雜的,我們還是一段一段的分析。
@Override
public void run() {
if (waitIfPaused()) return;
if (delayIfNeed()) return;
...
}
如果waitIfPaused(), delayIfNeed()返回true的話,直接從run()方法中返回了,不執(zhí)行下面的邏輯, 接下來我們先看看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);
}
}
滑動停止的話會調(diào)用到imageLoader.pause
public void pause() {
engine.pause();
}
...
void pause() {
paused.set(true);
}
這里的pause是
private final AtomicBoolean paused = new AtomicBoolean(false);
所以調(diào)用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去加載圖片,而直接加載當前界面的圖片。
回頭繼續(xù)看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又會去加載一次圖片,假設在很短的時間內(nèi)滾動很頻繁,那么就會出現(xiàn)多次去網(wǎng)絡上面請求圖片,所以這里根據(jù)圖片的Url去對應一個ReentrantLock對象,讓具有相同Url的請求就會在第10行等待,等到這次圖片加載完成之后,ReentrantLock就被釋放,剛剛那些相同Url的請求就會繼續(xù)執(zhí)行第10行下面的代碼。
之后來到第13行,先調(diào)用checkTaskNotActual判斷當前View是否被GC回收使用,是則拋出異常。
接著15行,它們會先從內(nèi)存緩存中獲取一遍,如果內(nèi)存緩存中沒有在去執(zhí)行下面的邏輯,所以ReentrantLock的作用就是避免這種情況下重復的去從網(wǎng)絡上面請求圖片。
17行的方法tryLoadBitmap(),這個方法確實也有點長,我先告訴大家,這里面的邏輯是先從文件緩存中獲取有沒有Bitmap對象,如果沒有在去從網(wǎng)絡中獲取,然后將bitmap保存在文件系統(tǒng)中,我們還是具體分析下
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行會去磁盤緩存中去獲取圖片,如果圖片已經(jīng)保存在磁盤了,則直接獲取對應的File路徑,調(diào)用bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));進行解析。
如果在磁盤中沒有的話,則到了12行,開始進行網(wǎng)絡下載獲取。
在17行會去調(diào)用isCacheOnDisk判斷是否要保持在磁盤中,如果默認false,如果是則調(diào)用tryCacheImageOnDisk來下載圖片并且保持在磁盤
private boolean tryCacheImageOnDisk() throws TaskCancelledException {
L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);
boolean loaded;
try {
loaded = downloadImage();
...
} ...
return loaded;
}
調(diào)用了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);
}
}
}
可以看到這里調(diào)用了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,如果有設置磁盤緩存的話,則會調(diào)用tryCacheImageOnDisk來下載并且保持圖片,此時的imageUriForDecoding則是文件File的路徑。
如果沒有設置磁盤緩存的話,則imageUriForDecoding還是uri。
關鍵則是在decodeImage,它能根據(jù)對應的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,它的默認實現(xiàn)類是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默認實現(xiàn)類是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);
}
}
可以看到,在這里,已經(jīng)做了多種情況的讀取判斷。第一篇文章就有介紹到UIL可以根據(jù)不同的uri來解析圖片,其原理就是在這里。
而前面通過tryCacheImageOnDisk來下載圖片也是根據(jù)這個。這里就不一一擴展開。
這里的網(wǎng)絡下載圖片內(nèi)部則是使用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被回收了或者被重用了,回調(diào)給ImageLoadingListener接口,否則就調(diào)用BitmapDisplayer去顯示Bitmap。到這里Bitmap已經(jīng)顯示加載完成,調(diào)用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線程池去處理,最終也是調(diào)用了LoadAndDisplayImageTask的run方法去加載顯示。
到這里Universal-Image-Loader的分析也算完了,從基本使用到內(nèi)存模型在加載顯示,可以看到UIL這個開源框架十分的靈活,比如建造者模式,裝飾模式,代理模式,策略模式等等,這樣方便我們?nèi)U展,實現(xiàn)我們想要的功能,當然,也帶給我們更多的想象空間。