本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨(dú)家發(fā)布,來源于我在CSDN發(fā)表的一篇博文:
http://blog.csdn.net/sinat_23092639/article/details/65936005
Universal-Image-Loader(項(xiàng)目地址)可以說是安卓知名圖片開源框架中最古老、使用率最高的一個(gè)了。一張圖片的加載對于安卓應(yīng)用的開發(fā)也許是件簡單的事,但是如果要同時(shí)加載大量的圖片,并且圖片用于ListView、GridView、ViewPager等控件,如何防止出現(xiàn)OOM、如何防止圖片錯(cuò)位(因?yàn)榱斜淼腣iew復(fù)用功能)、如何更快地加載、如何讓客戶端程序員用最簡單的操作完成本來十分復(fù)雜的圖片加載工作,成了全世界安卓應(yīng)用開發(fā)程序員心頭的一大難題,所幸有了Universal-Image-Loader,讓這一切變得簡單,從某種意義來講,它延長了安卓開發(fā)者的壽命~
針對上述幾個(gè)問題,Universal-Image-Loader可謂是兵來將擋水來土掩,見招拆招:
如何同時(shí)加載大量圖片:采用線程池優(yōu)化高
并發(fā)
如何提高加載速度:使用內(nèi)存、磁盤緩存
如何避免OOM:加載的圖片進(jìn)行壓縮,內(nèi)存緩存具有相關(guān)的淘汰算法
如何避免ListView的圖片錯(cuò)位:將要加載的圖片和要顯示的ImageView綁定, 在請求過程中判斷該ImageView當(dāng)前綁定的圖片是否是當(dāng)前請求的圖片,不是則停止當(dāng)前請求。
首先來看下Universal-Image-Loader整體流程圖:
總的來說就是:下載圖片——將圖片緩存在磁盤中——解碼圖片成為Bitmap——Bitmap的預(yù)處理——緩存在Bitmap內(nèi)存中——Bitmap的后期處理——顯示Bitmap
基本用法:
想必很多朋友也知道了,不過還是“抄襲”下Github說明里的使用方法:
**簡單版本:**
ImageLoader imageLoader = ImageLoader.getInstance(); // Get singleton instance
// Load image, decode it to Bitmap and display Bitmap in //ImageView (or any other view
// which implements ImageAware interface)
imageLoader.displayImage(imageUri, imageView);
// Load image, decode it to Bitmap and return Bitmap to callback
imageLoader.loadImage(imageUri, new SimpleImageLoadingListener() {
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
// Do whatever you want with Bitmap
}
});
// Load image, decode it to Bitmap and return Bitmap //synchronously
Bitmap bmp = imageLoader.loadImageSync(imageUri);
**復(fù)雜版本**:
// Load image, decode it to Bitmap and display Bitmap in ImageView (or any other view
// which implements ImageAware interface)
imageLoader.displayImage(imageUri, imageView, options, 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) {
...
}
@Override
public void onLoadingCancelled(String imageUri, View view) {
...
}
}, new ImageLoadingProgressListener() {
@Override
public void onProgressUpdate(String imageUri, View view, int current, int total) {
...
}
});
// Load image, decode it to Bitmap and return Bitmap to callback
ImageSize targetSize = new ImageSize(80, 50);
// result Bitmap will be fit to this size
imageLoader.loadImage(imageUri, targetSize, options, new SimpleImageLoadingListener() {
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
// Do whatever you want with Bitmap
}
});
// Load image, decode it to Bitmap and return Bitmap
//synchronously
ImageSize targetSize = new ImageSize(80, 50);
// result Bitmap will be fit to this size
Bitmap bmp = imageLoader.loadImageSync(imageUri, targetSize, options);
相信各位經(jīng)驗(yàn)豐富的安卓開發(fā)者一看就心照不宣,就不必解釋什么了。
和源碼見面之前,先和幾個(gè)重要的類打招呼:
ImageLoaderEngine:任務(wù)分發(fā)器,負(fù)責(zé)分發(fā)LoadAndDisplayImageTask和ProcessAndDisplayImageTask給具體的線程池去執(zhí)行。
ImageAware:顯示圖片的對象,可以是ImageView等。
ImageDownloader:圖片下載器,負(fù)責(zé)從圖片的各個(gè)來源獲取輸入流。
Cache:圖片緩存,分為MemoryCache和DiskCache兩部分。
MemoryCache:內(nèi)存圖片緩存,可向內(nèi)存緩存緩存圖片或從內(nèi)存緩存讀取圖片。
DiskCache:本地圖片緩存,可向本地磁盤緩存保存圖片或從本地磁盤讀取圖片。
ImageDecoder:圖片解碼器,負(fù)責(zé)將圖片輸入流InputStream轉(zhuǎn)換為Bitmap對象。
BitmapProcessor:圖片處理器,負(fù)責(zé)從緩存讀取或?qū)懭肭皩D片進(jìn)行處理。
BitmapDisplayer:將Bitmap對象顯示在相應(yīng)的控件ImageAware上。
LoadAndDisplayImageTask:用于加載并顯示圖片的任務(wù)。
ProcessAndDisplayImageTask:用于處理并顯示圖片的任務(wù)。
DisplayBitmapTask:用于顯示圖片的任務(wù)。
其中有個(gè)全局圖片加載配置類貫穿整個(gè)框架,ImageLoaderConfiguration,可以配置的東西實(shí)在有點(diǎn)多:
private ImageLoaderConfiguration(final Builder builder) {
resources = builder.context.getResources();
maxImageWidthForMemoryCache = builder.maxImageWidthForMemoryCache;
maxImageHeightForMemoryCache = builder.maxImageHeightForMemoryCache;
maxImageWidthForDiskCache = builder.maxImageWidthForDiskCache;
maxImageHeightForDiskCache = builder.maxImageHeightForDiskCache;
processorForDiskCache = builder.processorForDiskCache;
taskExecutor = builder.taskExecutor;
taskExecutorForCachedImages = builder.taskExecutorForCachedImages;
threadPoolSize = builder.threadPoolSize;
threadPriority = builder.threadPriority;
tasksProcessingType = builder.tasksProcessingType;
diskCache = builder.diskCache;
memoryCache = builder.memoryCache;
defaultDisplayImageOptions = builder.defaultDisplayImageOptions;
downloader = builder.downloader;
decoder = builder.decoder;
customExecutor = builder.customExecutor;
customExecutorForCachedImages = builder.customExecutorForCachedImages;
networkDeniedDownloader = new NetworkDeniedImageDownloader(downloader);
slowNetworkDownloader = new SlowNetworkImageDownloader(downloader);
L.writeDebugLogs(builder.writeLogs);
}
主要是圖片最大尺寸、線程池、緩存、下載器、解碼器等等。
經(jīng)過前面那么多的鋪墊,終于迎來了源碼~~
整個(gè)框架的源碼上萬,全部講完不可能,最好的方式還是按照加載流程走一遍,細(xì)枝末節(jié)各位可以自己慢慢研究,一旦整體把握好了,其他的一切就水到渠成,切勿只見樹木不見森林,迷失在各種代碼細(xì)節(jié)中~~
好了,簡單點(diǎn),講代碼的方式簡單點(diǎn),從最簡單的代碼切入:
imageLoader.displayImage(imageUri, imageView);
進(jìn)入方法:
public void displayImage(String uri, ImageView imageView) {
displayImage(uri, new ImageViewAware(imageView), null, null, null);
}
ImageAware是ImageView的包裝類,持有ImageView對象的弱引用,防止ImageView出現(xiàn)內(nèi)存泄漏發(fā)生。主要是提供了獲取ImageView寬度高度和ScaleType等。
最終會執(zhí)行這一個(gè)重載方法:
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);
}
//使用默認(rèn)的Listener
if (listener == null) {
listener = defaultListener;
}
//使用默認(rèn)的options
if (options == null) {
options = configuration.defaultDisplayImageOptions;
}
//uri為空
if (TextUtils.isEmpty(uri)) {
//則移除當(dāng)前請求
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;
}
//使用默認(rèn)的圖片尺寸
if (targetSize == null) {
targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
}
//根據(jù)uri和圖片尺寸生成緩存的key
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
//從內(nèi)存緩存取出Bitmap
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
//命中內(nèi)存緩存的相應(yīng)的Bitmap
if (bmp != null && !bmp.isRecycled()) {
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
//進(jìn)行Bitmap后期處理
if (options.shouldPostProcess()) {
//創(chuàng)建一個(gè)加載和顯示圖片任務(wù)需要的信息的對象
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
//將內(nèi)存緩存中取出的Bitmap顯示出來
ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
defineHandler(options));
//是否同時(shí)加載,否則使用線程池加載
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
} else {
//不進(jìn)行圖片處理則直接顯示在ImageView
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
} else {
//如果內(nèi)存緩存拿不到圖片
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));
//創(chuàng)建加載顯示圖片任務(wù)
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
defineHandler(options));
//也是分同步和異步兩種
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
}
}
這個(gè)方法基本描繪出了整個(gè)圖片加載的流程,重要的地方已經(jīng)加上注釋。
第8行的listener就是上面基本使用說明復(fù)雜版本的進(jìn)度回調(diào)接口ImageLoadingListener,大家看下就知道,如果沒有配置的話設(shè)置為默認(rèn),而默認(rèn)其實(shí)啥都沒做,方法都是空實(shí)現(xiàn)。
第12行也是類似地如果沒有配置DisplayImageOptions,即圖片顯示的配置項(xiàng),則取默認(rèn)的。
看下這個(gè)圖片顯示配置類可以配置什么:
/** Sets all options equal to incoming options */
public Builder cloneFrom(DisplayImageOptions options) {
imageResOnLoading = options.imageResOnLoading;
imageResForEmptyUri = options.imageResForEmptyUri;
imageResOnFail = options.imageResOnFail;
imageOnLoading = options.imageOnLoading;
imageForEmptyUri = options.imageForEmptyUri;
imageOnFail = options.imageOnFail;
resetViewBeforeLoading = options.resetViewBeforeLoading;
cacheInMemory = options.cacheInMemory;
cacheOnDisk = options.cacheOnDisk;
imageScaleType = options.imageScaleType;
decodingOptions = options.decodingOptions;
delayBeforeLoading = options.delayBeforeLoading;
considerExifParams = options.considerExifParams;
extraForDownloader = options.extraForDownloader;
preProcessor = options.preProcessor;
postProcessor = options.postProcessor;
displayer = options.displayer;
handler = options.handler;
isSyncLoading = options.isSyncLoading;
return this;
}
配置是否內(nèi)存磁盤緩存以及設(shè)置圖片預(yù)處理和后期處理器(預(yù)處理和后期處理器默認(rèn)為null,留給客戶端程序員擴(kuò)展)等。
displayImage方法第27行里,如果沒有專門設(shè)置targetSize,即指定圖片的尺寸,就取ImageAware的寬高,即包裝在里面的ImageView的寬高,如果得到的寬高小于等于0,則設(shè)置為最大寬高,如果沒有設(shè)置內(nèi)存緩存的最大寬高(maxImageWidthForMemoryCache,maxImageHeightForMemoryCache),則為屏幕的寬高。
然后根據(jù)圖片uri和圖片的尺寸生成一個(gè)內(nèi)存緩存的key,之所以使用圖片尺寸是因?yàn)閮?nèi)存緩存的圖片同一張圖片擁有不同尺寸的版本。
displayImage方法第33行中:
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
engine指的是任務(wù)分發(fā)器 ImageLoaderEngine,它持有一個(gè)HashMap,作為記錄圖片加載任務(wù)使用,一旦任務(wù)停止加載或者加載完畢則會刪除對應(yīng)的任務(wù)引用。prepareDisplayTaskFor方法正是將任務(wù)的引用添加到該HashMap中,這里以內(nèi)存緩存的Key為鍵。
接下來如果拿到的圖片需要做后期處理,則創(chuàng)建一個(gè)圖片顯示信息的對象,然后以之前創(chuàng)建的圖片顯示信息的對象為參數(shù)之一創(chuàng)建一個(gè)處理顯示任務(wù)對象ProcessAndDisplayImageTask(實(shí)現(xiàn)Runnable),如果是指定同步加載則直接調(diào)用它的run方法,不是則將其添加到任務(wù)分發(fā)器ImageLoaderEngine的線程池中異步執(zhí)行。
若不需要對圖片進(jìn)行后期處理則調(diào)用Displayer去顯示顯示圖片,默認(rèn)的Displayer為SimpleBitmapDisplayer,只是簡單地顯示圖片到指定的ImageView中。
如果拿不到內(nèi)存緩存的對應(yīng)圖片,則創(chuàng)建加載顯示圖片任務(wù)對象LoadAndDisplayImageTask,然后執(zhí)行該任務(wù)去磁盤緩存或者網(wǎng)絡(luò)加載圖片。
接下來的關(guān)鍵就是看下LoadAndDisplayImageTask的run方法怎么執(zhí)行的,就知道整個(gè)圖片加載怎么運(yùn)行的了。
public void run() {
//當(dāng)前請求是否需要暫停等待,等待期間被中斷則停止請求
if (waitIfPaused()) return;
//如果當(dāng)前請求延時(shí)加載,延時(shí)期間被中斷則停止請求
if (delayIfNeed()) return;
//得到與圖片uri綁定的鎖
ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
if (loadFromUriLock.isLocked()) {
L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
}
//鎖與圖片uri對應(yīng)是為了防止同時(shí)加載同一張圖片
loadFromUriLock.lock();
Bitmap bmp;
try {
//檢查當(dāng)前任務(wù)的對應(yīng)Imageview是否被回收或者復(fù)用,是則拋TaskCancelledException結(jié)束當(dāng)前任務(wù)
checkTaskNotActual();
//疑問:再一次嘗試從內(nèi)存緩存中獲取是因?yàn)榭赡艽藭r(shí)其他任務(wù)加載了相同的圖片?
bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp == null || bmp.isRecycled()) {
//內(nèi)存緩存獲取不到,嘗試到磁盤緩存或者網(wǎng)絡(luò)獲取
bmp = tryLoadBitmap();
if (bmp == null) return; // listener callback already was fired
checkTaskNotActual();
//檢查任務(wù)是否被Interrupt
checkTaskInterrupted();
//圖片是否需要預(yù)處理
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);
//添加到內(nèi)存緩存
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);
}
}
//再次判斷是否要停止當(dāng)前任務(wù)
checkTaskNotActual();
checkTaskInterrupted();
} catch (TaskCancelledException e) {
fireCancelEvent();
return;
} finally {
loadFromUriLock.unlock();
}
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
runTask(displayBitmapTask, syncLoading, handler, engine);
}
un方法不長,就直接展示了。
這里第3、5行:
if (waitIfPaused()) return;
if (delayIfNeed()) return;
第1行中的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();
}
這里如果engine的pause標(biāo)志位被置位為true,則會調(diào)用engine.getPauseLock()對象(其實(shí)就是一個(gè)普通的Object對象)的wait方法使當(dāng)前線程暫停運(yùn)行。為什么要這樣呢?看下engine的pause標(biāo)志位什么時(shí)候會被置位為true,終于在PauseOnScrollListener的重寫方法onScrollStateChanged中找到:
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;
}
我們知道ListView滑動時(shí)候加載顯示圖片會使得滑動有卡頓現(xiàn)象。這就是在列表滑動時(shí)暫停加載的方式,只要給ListView設(shè)置:
ListView.setOnScrollListener(new PauseOnScrollListener(pauseOnScroll, pauseOnFling ))
就可以通過在滑動時(shí)候暫停請求任務(wù)從而防止ListView滑動帶來的卡頓。一旦ListView在滑動狀態(tài),所有新增的圖片請求都會暫停等待列表滑動停止調(diào)用imageLoader.resume()方法喚醒暫停的線程繼續(xù)加載請求。
waitIfPaused()在線程等待過程中被中斷(線程池被停止運(yùn)行)的時(shí)候會返回true,從而終止任務(wù)的run方法終止加載請求。它的返回值還由isTaskNotActual方法決定,該方法主要判斷顯示圖片的ImageView是否被回收以及所在的列表項(xiàng)是否被復(fù)用,是的話也是終止加載任務(wù)。
delayIfNeed方法則是在任務(wù)需要延遲的時(shí)候讓當(dāng)前線程sleep一會,同樣也是遇到中斷返回true終止任務(wù)。
接下來12行:
loadFromUriLock.lock();
這里的loadFromUriLock是一個(gè)ReentrantLock對象,是和要加載的圖片uri綁定一起的,所以相同uri的圖片具有同一把鎖,這也就意味著相同圖片的請求同一時(shí)間只有一個(gè)在網(wǎng)絡(luò)或者磁盤加載,有效避免了重復(fù)的網(wǎng)絡(luò)或者磁盤緩存加載,一旦加載完其余請求都會從內(nèi)存緩存中加載圖片。
然后嘗試從內(nèi)存緩存中取出圖片,一旦成功得到圖片,則經(jīng)過請求是否有效的相關(guān)判斷之后,創(chuàng)建一個(gè)DisplayBitmapTask將圖片顯示到對應(yīng)的ImageView中。
關(guān)鍵是如果內(nèi)存取不到圖片的情況,這時(shí)候就得看run中的22行:
bmp = tryLoadBitmap();
該方法就是去磁盤緩存取圖片,如果沒有則取網(wǎng)絡(luò)圖片。
方法也不長,所以也直接拷貝過來:
private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
try {
File imageFile = configuration.diskCache.get(uri);
//從文件系統(tǒng)中取到圖片
if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
loadedFrom = LoadedFrom.DISC_CACHE;
//檢查當(dāng)前任務(wù)的對應(yīng)Imageview是否被回收或者復(fù)用,是則拋TaskCancelledException結(jié)束當(dāng)前任務(wù)
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;
//磁盤緩存獲取失敗則從網(wǎng)絡(luò)獲取
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
imageFile = configuration.diskCache.get(uri);
if (imageFile != null) {
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
}
//仍然檢查當(dāng)前任務(wù)的對應(yīng)Imageview是否被回收或者復(fù)用,是則拋TaskCancelledException結(jié)束當(dāng)前任務(wù)
checkTaskNotActual();
//tryCacheImageOnDisk將圖片保存到文件系統(tǒng)。
//接下來將圖片按需求裁剪等處理后加載到內(nèi)存
//所以文件保存的是原圖或者縮略圖,內(nèi)存保存的是根據(jù)ImageView大小、scaletype、方向處理過得圖片
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;
}
首先第四行是嘗試從磁盤文件系統(tǒng)中尋找對應(yīng)的圖片,如果有的話,調(diào)用:
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
進(jìn)行解碼得到對應(yīng)的Bitmap對象,注意這里將文件路徑使用Scheme.FILE進(jìn)行包裝,由于Universal-Image-Loader加載圖片有多個(gè)來源,比如網(wǎng)絡(luò),文件系統(tǒng),項(xiàng)目文件夾asset等,所以解碼的時(shí)候進(jìn)行一個(gè)包裝,方便解碼器BaseImageDecoder在解碼的時(shí)候識別來源進(jìn)行相應(yīng)的獲取流處理。
Scheme總共有以下幾個(gè):
HTTP("http"), HTTPS("https"), FILE("file"), CONTENT("content"), ASSETS("assets"), DRAWABLE("drawable"), UNKNOWN
這里的decodeImage方法:
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);
}
根據(jù)ImageView的ScaleType以及targetSize等生成一個(gè)ImageDecodingInfo對象,傳入解碼器decoder的decode方法中。
這里的解碼器默認(rèn)為BaseImageDecoder,decode方法:
public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
Bitmap decodedBitmap;
ImageFileInfo imageInfo;
//獲得圖片對應(yīng)的流
InputStream imageStream = getImageStream(decodingInfo);
if (imageStream == null) {
L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey());
return null;
}
try {
//根據(jù)圖片的屬性生成一個(gè)ImageFileInfo對象,指定尺寸和旋轉(zhuǎn)處理
imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
// 還原stream
// 為什么還原呢? 因?yàn)橥ㄟ^上面的操作,我們的stream游標(biāo)可能
// 已經(jīng)不在首部,這時(shí)再去讀取會造成了信息不完整
// 所以這里需要reset一下
imageStream = resetStream(imageStream, decodingInfo);
Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
//解碼獲取Bitmap對象
decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
} finally {
IoUtils.closeSilently(imageStream);
}
if (decodedBitmap == null) {
L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
} else {
//對Bitmap進(jìn)行裁剪和旋轉(zhuǎn)等操作
decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
imageInfo.exif.flipHorizontal);
}
return decodedBitmap;
}
這里具體如何裁剪和旋轉(zhuǎn)的代碼就不介紹了,有興趣大家可以去源碼看下。最后得到的圖片就可以直接使用顯示圖片任務(wù)DisplayBitmapTask將圖片顯示在ImageView上了。
假如磁盤也獲取不到圖片,那就需要到網(wǎng)絡(luò)加載了。
tryLoadBitmap方法的20行:
if (options.isCacheOnDisk() && tryCacheImageOnDisk())
判斷是否需要磁盤緩存,需要的話,進(jìn)入tryCacheImageOnDisk方法:
private boolean tryCacheImageOnDisk() throws TaskCancelledException {
L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);
boolean loaded;
try {
//從網(wǎng)絡(luò)加載圖片并緩存到文件系統(tǒng)中
loaded = downloadImage();
if (loaded) {
//將原圖轉(zhuǎn)化為縮略圖(疑問:不可以先判斷是否壓縮,再緩存圖片到文件系統(tǒng)?)
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;
}
主要就是通過downloadImage方法從網(wǎng)絡(luò)加載圖片,保存到文件系統(tǒng)中。然后判斷是否配置了maxImageWidthForDiskCache和maxImageHeightForDiskCache選項(xiàng),有的話對保存在文件系統(tǒng)的圖片進(jìn)行裁剪壓縮得到縮略圖。
然后在tryLoadBitmap方法第21行中,取出對應(yīng)磁盤緩存的圖片文件路徑,然后使用解碼器將圖片解碼文件為Bitmap。
以上討論的是需要磁盤緩存的情況,如果不需要呢?看下tryLoadBitmap方法第31行:
bitmap = decodeImage(imageUriForDecoding);
imageUriForDecoding是圖片的uri,該方法上面已經(jīng)講過,最終是通過流解碼為一個(gè)Bitmap對象,也就是從網(wǎng)絡(luò)加載圖片到內(nèi)存,而前面講的是從文件系統(tǒng)加載圖片到內(nèi)存。
這里需要注意的是,磁盤緩存的是原圖或者其縮略圖,內(nèi)存緩存的是根據(jù)ImageView裁剪的圖片。
拿到Bitmap對象,然后就是對圖片進(jìn)行預(yù)處理了(如果配置為需要的話),處理完畢后添加到內(nèi)存緩存,然后進(jìn)行后期處理(如果需要的話),最后將Bitmap交給顯示任務(wù)DisplayBitmapTask處理。
DisplayBitmapTask的run就十分簡單了:
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沒有被回收和復(fù)用的情況下,交給顯示器displayer處理,displayer默認(rèn)為SimpleBitmapDisplayer,SimpleBitmapDisplayer的display其實(shí)就是直接調(diào)用ImageView的setBitmapImage方法將圖片顯示上去。然后將該ImageView的圖片加載任務(wù)從任務(wù)分發(fā)器任務(wù)記錄中移除。
DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
runTask(displayBitmapTask, syncLoading, handler, engine);
這里注意下runTask的參數(shù)handler,它的來源在ImageLoader類的defineHandler方法:
private static Handler defineHandler(DisplayImageOptions options) {
Handler handler = options.getHandler();
if (options.isSyncLoading()) {
handler = null;
} else if (handler == null && Looper.myLooper() == Looper.getMainLooper()) {
handler = new Handler();
}
return handler;
}
可以看到首先取顯示配置中的Handler,默認(rèn)為null,所以默認(rèn)會執(zhí)行下面的判斷,如果調(diào)用該方法是在主線程,則創(chuàng)建一個(gè)Handler對象,于是改Handler就和主線程綁定一起了(不熟悉Hanlder的朋友可以看下我的另一篇博文:全面分析Handler消息機(jī)制)
最后會利用Handler的post方法將圖片的顯示傳遞到主線程調(diào)用~~
圖片加載過程算是Over了~~
這里談下一些注意要點(diǎn):
1.關(guān)于線程池:
當(dāng)加載顯示任務(wù)要執(zhí)行的時(shí)候,任務(wù)分發(fā)器的submit對應(yīng)方法是:
void submit(final LoadAndDisplayImageTask task) {
//任務(wù)分發(fā)的線程池
taskDistributor.execute(new Runnable() {
@Override
public void run() {
File image = configuration.diskCache.get(task.getLoadingUri());
boolean isImageCachedOnDisk = image != null && image.exists();
initExecutorsIfNeed();
if (isImageCachedOnDisk) {
//加載緩存圖片的線程池
taskExecutorForCachedImages.execute(task);
} else {
//加載源圖片線程池
taskExecutor.execute(task);
}
}
});
}
其中任務(wù)分發(fā)線程池taskDistributor為緩存線程池(CacheThreadPoll),用于判斷任務(wù)請求的圖片是否緩存在磁盤中以及分發(fā)任務(wù)給執(zhí)行任務(wù)線程池。兩個(gè)實(shí)際執(zhí)行任務(wù)的線程池taskExecutorForCachedImages和taskExecutor為可配置的線程池,默認(rèn)核心和工作線程數(shù)都為3,默認(rèn)配置采用的先進(jìn)先出的隊(duì)列,如果是列表圖片建議配置為先進(jìn)后出隊(duì)列。兩個(gè)線程池可以根據(jù)任務(wù)性質(zhì)自定義配置擴(kuò)展其他屬性。
為什么要專門分為三個(gè)線程池而不是一個(gè)線程池執(zhí)行所有任務(wù)呢?如果所有任務(wù)運(yùn)行在一個(gè)線程池中,所有的任務(wù)就都只能采取同一種任務(wù)優(yōu)先級和運(yùn)行策略。顯然果要有更好的性能,在線程數(shù)比較多并且線程承擔(dān)的任務(wù)不同的情況下,App中最好還是按任務(wù)的類別來劃分線程池。
一般來說,任務(wù)分為CPU密集型任務(wù),IO密集型任務(wù)和混合型任務(wù)。CPU密集型任務(wù)配置盡可能小的線程,如配置Ncpu+1個(gè)線程的線程池。IO密集型任務(wù)則由于線程并不是一直在執(zhí)行任務(wù),則配置盡可能多的線程,如2*Ncpu。具體可參見從源代碼分析Universal-Image-Loader中的線程池
2.關(guān)于下載器:在加載顯示任務(wù)類LoadAndDisplayImageTask的獲取下載器方法中:
private ImageDownloader getDownloader() {
ImageDownloader d;
if (engine.isNetworkDenied()) {
d = networkDeniedDownloader;
} else if (engine.isSlowNetwork()) {
d = slowNetworkDownloader;
} else {
d = downloader;
}
return d;
}
默認(rèn)網(wǎng)絡(luò)良好的情況下使用BaseImageDownloader。在無網(wǎng)絡(luò)情況下使用NetworkDeniedImageDownloader,當(dāng)判斷到圖片源為網(wǎng)絡(luò)時(shí)拋出異常停止請求。網(wǎng)絡(luò)不佳情況下使用SlowNetworkDownloader,主要通過重寫FilterInputStream的 skip(long n) 函數(shù)解決在慢網(wǎng)絡(luò)情況下 decode image 異常的 Bug。(具體解決原理本人也不太清楚,求教。。)
另外關(guān)于緩存機(jī)制具體可以看下從源代碼分析Android-Universal-Image-Loader的緩存處理機(jī)制
總結(jié)整個(gè)框架的特點(diǎn):1.支持內(nèi)存和磁盤兩級緩存。2.配置高靈活性,包括緩存算法、線程池屬性、顯示選項(xiàng)等等。3.支持同步異步加載4.運(yùn)用多種設(shè)計(jì)模式,比如模板方法、裝飾者、建造者模式等等,具有很高的可擴(kuò)展性可復(fù)用性。
好了,這個(gè)開源框架就講到這里,希望對大家有幫助,個(gè)人水平有限,閱讀開源框架源碼還不是很多,不足之處請幫忙糾正~~
參考文章:
Android 開源框架Universal-Image-Loader完全解析(三)—源代碼解讀
Android Universal Image Loader 源碼分析
從源代碼分析Universal-Image-Loader中的線程池