前言
提到圖片加載框架,最經典的應該就是Universal-Image-Loader了。此篇文章不會涉及如何使用,而是通過查看源碼的方式對框架的原理進行理解。
源碼解析
當我們要顯示一張圖片時,實際上只需要一行代碼就可以做到:
ImageLoader.getInstance().displayImage(address,imgv);
getInstance()很顯然是一個單例模式,那么我們跟進去displayImage方法:
public void displayImage(String uri, ImageView imageView) {
displayImage(uri, new ImageViewAware(imageView), null, null, null);
}
在這里創建了一個ImageViewAware對象。
public ImageViewAware(ImageView imageView) {
super(imageView);
}
//他的父類是ViewAware,貼上父類的構造函數:
public ViewAware(View view) {
this(view, true);
}
public ViewAware(View view, boolean checkActualViewSize) {
if (view == null) throw new IllegalArgumentException("view must not be null");
this.viewRef = new WeakReference<View>(view);
this.checkActualViewSize = checkActualViewSize;
}
這里涉及到了一個概念:強引用、弱引用、虛引用的區別。
- 強引用。我們平時使用的引用默認都是強引用。如果一個對象可以被某個強引用找到(即存在一個強引用指向該對象),那么這個對象一定不會被Java虛擬機所回收。如果當內存不夠時,強引用指向的對象又無法被回收,那么,就會拋出OutOfMemory異常。
- 弱引用(SoftReference)。如果一個對象不存在指向它的強引用,并且存在指向它的弱引用。那么,當內存不夠時,Java虛擬機會將該對象回收。而如果內存充足,那么就不會回收該對象。
- 虛引用(WeakReference)。如果一個對象不存在指向它的強引用和弱引用,并且存在指向它的虛引用。當虛擬機進行垃圾回收時,無論此時內存是否充足,這個對象都會被回收。
看到這里,我想大家就會明白了,使用虛引用能夠有效的避免內存泄露。
我們繼續查看displayImage方法,代碼比較長,把源碼的解釋寫到了注釋里:
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
displayImage(uri, imageAware, options, null, listener, progressListener);
}
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
checkConfiguration(); //用來檢查是否對ImageLoader進行了配置,只需要在Application中初始化一次
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;
}
if (targetSize == null) {
//在獲取圖片之前,首先查看ImageView的大小
targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
}
//使用url和大小作為主鍵
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
//使用HashMap將ImageView的id和主鍵對應存放起來,等效于為ImageView設置tag,從而解決ListView圖片亂序等問題。
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);
}
} 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);
}
}
}
重要步驟的方法的解釋已經寫到了注釋里,就不再贅述了。首先來看一下defineTargetSizeForView方法。
public static ImageSize defineTargetSizeForView(ImageAware imageAware, ImageSize maxImageSize) {
int width = imageAware.getWidth();
if (width <= 0) width = maxImageSize.getWidth();
int height = imageAware.getHeight();
if (height <= 0) height = maxImageSize.getHeight();
return new ImageSize(width, height);
}
public int getWidth() { ///ImageViewAware類
int width = super.getWidth();
if (width <= 0) {
ImageView imageView = (ImageView) viewRef.get();
if (imageView != null) {
width = getImageViewFieldValue(imageView, "mMaxWidth"); // Check maxWidth parameter
}
}
return width;
}
public int getWidth() { //ViewAware類
View view = viewRef.get();
if (view != null) {
final ViewGroup.LayoutParams params = view.getLayoutParams();
int width = 0;
if (checkActualViewSize && params != null && params.width != ViewGroup.LayoutParams.WRAP_CONTENT) {
width = view.getWidth(); // Get actual image width
}
if (width <= 0 && params != null) width = params.width; // Get layout width parameter
return width;
}
return 0;
}
寬度和高度的邏輯是一樣的,在這里以寬度為例。在ViewAware類的getWidth()方法中,可以看到,如果ImageView的寬度不是wrap_content,那么就返回ImageView的寬度;如果ImageView的寬度是wrap_content,那么獲取寬度的邏輯應該轉移到ImageViewAware類的getWidth方法,如果ImageView設置了maxWidth這個屬性,那么就返回maxWidth,否則獲取寬度的邏輯應該轉移到defineTargetSizeForView類中,maxImageSize.getWidth()方法返回的數值,實際上是我們在初始化ImageLoader設置的參數。
接下來我們來看一下最主要的流程的代碼,也就是displayImage最底部的代碼。很容易看出來,LoadAndDisplayImageTask應該是一個Thread,跟進去看一下run方法。
public void run() {
if (waitIfPaused()) return;
if (delayIfNeed()) return;
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();
}
//將圖像顯示在ImageView中
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
runTask(displayBitmapTask, syncLoading, handler, engine);
}
首先我們來看一下tryLoadBitmap方法。
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()) { //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);
}
}
} catch (IllegalStateException e) {
fireFailEvent(FailType.NETWORK_DENIED, null);
} catch (TaskCancelledException e) {
throw e;
} catch (IOException e) {
L.e(e);
fireFailEvent(FailType.IO_ERROR, e);
} catch (OutOfMemoryError e) {
L.e(e);
fireFailEvent(FailType.OUT_OF_MEMORY, e);
} catch (Throwable e) {
L.e(e);
fireFailEvent(FailType.UNKNOWN, e);
}
return bitmap;
}
我們來看一下tryCacheImageOnDisk方法:
private boolean tryCacheImageOnDisk() throws TaskCancelledException {
L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);
boolean loaded;
try {
loaded = downloadImage();
if (loaded) {
int width = configuration.maxImageWidthForDiskCache;
int height = configuration.maxImageHeightForDiskCache;
if (width > 0 || height > 0) {
L.d(LOG_RESIZE_CACHED_IMAGE_FILE, memoryCacheKey);
resizeAndSaveImage(width, height); // TODO : process boolean result
}
}
} catch (IOException e) {
L.e(e);
loaded = false;
}
return loaded;
}
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);
}
}
}
private boolean resizeAndSaveImage(int maxWidth, int maxHeight) throws IOException {
// Decode image file, compress and re-save it
boolean saved = false;
File targetFile = configuration.diskCache.get(uri);
if (targetFile != null && targetFile.exists()) {
ImageSize targetImageSize = new ImageSize(maxWidth, maxHeight);
DisplayImageOptions specialOptions = new DisplayImageOptions.Builder().cloneFrom(options)
.imageScaleType(ImageScaleType.IN_SAMPLE_INT).build();
ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey,
Scheme.FILE.wrap(targetFile.getAbsolutePath()), uri, targetImageSize, ViewScaleType.FIT_INSIDE,
getDownloader(), specialOptions);
Bitmap bmp = decoder.decode(decodingInfo);
if (bmp != null && configuration.processorForDiskCache != null) {
L.d(LOG_PROCESS_IMAGE_BEFORE_CACHE_ON_DISK, memoryCacheKey);
bmp = configuration.processorForDiskCache.process(bmp);
if (bmp == null) {
L.e(ERROR_PROCESSOR_FOR_DISK_CACHE_NULL, memoryCacheKey);
}
}
if (bmp != null) {
//將圖像保存到硬盤中
saved = configuration.diskCache.save(uri, bmp);
bmp.recycle();
}
}
return saved;
}
通過上面的分析,我們知道了如何獲取到Bitmap。然后來看一下將Bitmap顯示出來的部分。
final class DisplayBitmapTask implements Runnable {
.......
@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);
//將圖像顯示在ImageView中
displayer.display(bitmap, imageAware, loadedFrom);
//從HashMap中,將ImageView的id和url組成的鍵值對刪去
engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
}
}
/** Checks whether memory cache key (image URI) for current ImageAware is actual */
private boolean isViewWasReused() {
String currentCacheKey = engine.getLoadingUriForView(imageAware);
return !memoryCacheKey.equals(currentCacheKey);
}
}
到這里,整個流程就分析完了~希望讓你有所收獲~