從原始碼角度分析imageLoader框架
阿新 • • 發佈:2019-02-16
本文來自http://blog.csdn.net/andywuchuanlong,轉載請說明出處
對於圖片的載入和處理基本上是Android應用軟體專案中的常客,很多初學者在遇到圖片載入這個問題是,總是喜歡自己寫一個http請求,然後使用將流轉換成bitmap,從而顯示在專案的view中。其實對於圖片的處理自己寫固然是好,但是要想軟體穩定的執行,裡面還是需要很多細節東西需要處理的。在github上有很多的開源專案,處理圖片的也不少,下面介紹一下imageLoader這個開源框架。
接觸Imageloader這個框架已經很久了,在專案總也使用過,但是僅僅是使用,作為一個開發人員而言,雖然不提倡重複的造輪子,但是對於利用現有的輪子我們應該還是要知其所以然,才能將這個輪子的製造工藝轉成我麼自己的技術。廢話不多說,我們一起從原始碼的角度來認識imageLoader。 在使用imageLoader載入圖片之前,我們必須要先初始化一個loader:ImageLoader是一個單例,也就是說在一個專案中只會有一個imageLaoder存在。例項化ImageLoader之後緊接著使用imageLoader.init(ImageLoaderConfiguration)給圖片載入器設定一些配置項並且初始化imageLoaderEngine引擎。這個配置裡面指明瞭記憶體中圖片的最大寬度和最大高度、任務執行器等:<span style="white-space:pre"> </span>public static ImageLoader getInstance() { if (instance == null) { synchronized (ImageLoader.class) { if (instance == null) { instance = new ImageLoader(); } } } return instance; }
配置中涉及到的具體屬性在後面的原始碼中都會涉及到,再具體分析。 上面兩部分做完之後,就可以開始載入圖片了,從ImageLoader的displayImage方法下手分析。在displayImage中首先是檢查有沒有給ImageLoader設定一些載入配置項。然後就是設定圖片載入過程的監聽,這個監聽器可以監聽圖片載入的開始、取消、結束,這樣我麼就可以很靈活的使用它了。接下來就是判斷要載入的圖片uri是否為空了,為空就不去載入,但是這裡還做了一個取消即將要顯示的imageview,然後開始載入在配置裡面指定的預設圖片並顯示,最後通知監聽器執行onLoadingComplete方法:private ImageLoaderConfiguration(final Builder builder) { context = builder.context; // 記憶體中的圖片最大寬度 maxImageWidthForMemoryCache = builder.maxImageWidthForMemoryCache; // 記憶體中圖片的最大高度 maxImageHeightForMemoryCache = builder.maxImageHeightForMemoryCache; maxImageWidthForDiscCache = builder.maxImageWidthForDiscCache; maxImageHeightForDiscCache = builder.maxImageHeightForDiscCache; imageCompressFormatForDiscCache = builder.imageCompressFormatForDiscCache; // 硬碟快取中圖片的質量 imageQualityForDiscCache = builder.imageQualityForDiscCache; // 任務執行器 taskExecutor = builder.taskExecutor; taskExecutorForCachedImages = builder.taskExecutorForCachedImages; // 執行緒池的大小 threadPoolSize = builder.threadPoolSize; // 執行緒的優先順序 threadPriority = builder.threadPriority; // 任務處理型別 tasksProcessingType = builder.tasksProcessingType; discCache = builder.discCache; memoryCache = builder.memoryCache; // 圖片顯示選項 defaultDisplayImageOptions = builder.defaultDisplayImageOptions; loggingEnabled = builder.loggingEnabled; downloader = builder.downloader; decoder = builder.decoder; customExecutor = builder.customExecutor; customExecutorForCachedImages = builder.customExecutorForCachedImages; networkDeniedDownloader = new NetworkDeniedImageDownloader(downloader); // 網路緩慢下的情況下圖片的下載器 slowNetworkDownloader = new SlowNetworkImageDownloader(downloader); reserveDiscCache = DefaultConfigurationFactory.createReserveDiscCache(context); }
如果uri不為空,則要通知引擎準備載入圖片,並把imageview和imageview在快取中對應的key大小作為引數傳入<span style="white-space:pre"> </span>if (uri == null || uri.length() == 0) { // 取消圖片顯示,取消是根據imageview的hashCode來取消的 // engine內部維護一個cacheKeysForImageViews,是一個map,key為imageView的hashcode,value為memoryCacheKey engine.cancelDisplayTaskFor(imageView); // 開始載入圖片 listener.onLoadingStarted(uri, imageView); if (options.shouldShowImageForEmptyUri()) { imageView.setImageResource(options.getImageForEmptyUri()); } else { imageView.setImageBitmap(null); } listener.onLoadingComplete(uri, imageView, null); return; }
<span style="white-space:pre"> </span>ImageSize targetSize = ImageSizeUtils.defineTargetSizeForView(imageView, configuration.maxImageWidthForMemoryCache,
configuration.maxImageHeightForMemoryCache);
String memoryCacheKey = MemoryCacheUtil.generateKey(uri, targetSize);
// 通知引擎準備載入圖片,並把圖片的快取的唯一識別key傳入
engine.prepareDisplayTaskFor(imageView, memoryCacheKey);
上面的操作做完之後就開始載入了,首先判斷記憶體中是否存在該圖片,如果圖片存在並且沒有被置為回收的狀態則顯示圖片,顯示之前,判斷是否需要對圖片進行額外的處理,這個實在配置項中進行配置的,如果需要在顯示前自己可以對圖片進行處理就需要實現BitmapProcessor,並重寫process(Bitmap
bitmap)方法。在ProcessAndDisplayImageTask類中:
<span style="white-space:pre"> </span>@Override
public void run() {
if (engine.configuration.loggingEnabled) L.i(LOG_POSTPROCESS_IMAGE, imageLoadingInfo.memoryCacheKey);
BitmapProcessor processor = imageLoadingInfo.options.getPostProcessor();
final Bitmap processedBitmap = processor.process(bitmap);
if (processedBitmap != bitmap) {
bitmap.recycle();
}
handler.post(new DisplayBitmapTask(processedBitmap, imageLoadingInfo, engine));
}
根據配置項中指定圖片處理器處理圖片,處理完之後再顯示,顯示圖片有四種策略,這些策略也是可以配置的。在DisplayBitmapTask類中首先判斷圖片是否錯位,然後再顯示圖片
<span style="white-space:pre"> </span>public void run() {
if (isViewWasReused()) {
if (loggingEnabled) L.i(LOG_TASK_CANCELLED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageView);
} else {
if (loggingEnabled) L.i(LOG_DISPLAY_IMAGE_IN_IMAGEVIEW, memoryCacheKey);
/**
* 開始顯示圖片,有四種顯示策略
* SimpleBitmapDisplayer:簡單的直接顯示圖片,setImageBitmap(imageView)
* RoundedBitmapDisplayer : 圓角圖片顯示,圓角處理roundCorners方法
* FadeInBitmapDisplayer:顯示的時候使用fade in動畫
* FakeBitmapDisplayer: 假動作顯示,也就是不顯示圖片
*/
Bitmap displayedBitmap = displayer.display(bitmap, imageView);
listener.onLoadingComplete(imageUri, imageView, displayedBitmap);
engine.cancelDisplayTaskFor(imageView);
}
}
如果不需要額外處理圖片的話就直接顯示圖片。
<span style="white-space:pre"> </span>if (bmp != null && !bmp.isRecycled()) {
// 如果圖片不為空,並且沒有被回收,則可以直接顯示
if (configuration.loggingEnabled) L.i(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
// 判斷圖片是否需要額外的處理 ,是否需要使使用者自己配置的
// 如果需要在現實之前做另外的處理,可以實現介面BitmapProcessor,並重寫process(Bitmap bitmap)方法
if (options.shouldPostProcess()) {
// 一個實體類,裡面持有uri、imageview、size、快取key、配置選項等屬性
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageView, targetSize,
memoryCacheKey, options, listener,engine.getLockForUri(uri));
// 處理圖片並且顯示圖片,這個是runnable,在裡面又由handler執行了post(DisplayBitmapTask)
ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp,
imageLoadingInfo, options.getHandler());
engine.submit(displayTask);
} else {
// 顯示圖片
options.getDisplayer().display(bmp, imageView);
// 通知監聽器載入完畢
listener.onLoadingComplete(uri, imageView, bmp);
}
}
如果圖片不存在快取中,就需要嘗試從硬碟和網路中載入了,載入之前判斷是否需要在載入的過程中顯示預設的圖片,然後開啟LoadAndDisplayImageTask自行任務
<span style="white-space:pre"> </span>// 記憶體快取中不存在圖片,需要進行網路載入
// 判斷載入圖的過程中是否需要顯示圖片
if (options.shouldShowStubImage()) {
imageView.setImageResource(options.getStubImage());
} else {
if (options.isResetViewBeforeLoading()) {
<span style="white-space:pre"> </span>imageView.setImageBitmap(null);
}
}
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageView, targetSize, memoryCacheKey, options, listener, engine.getLockForUri(uri));
// 載入和顯示圖片的任務,載入策略:先從快取中查詢圖片,再從硬碟中查詢,再從網路中載入
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo, options.getHandler());
engine.submit(displayTask);
在LoadAndDisplayImageTask類的run方法中:
<span style="white-space:pre"> </span>@Override
public void run() {
//是否需要等待
if (waitIfPaused()) return;
// 是否需要延時
if (delayIfNeed()) return;
ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
log(LOG_START_DISPLAY_IMAGE_TASK);
if (loadFromUriLock.isLocked()) {
log(LOG_WAITING_FOR_IMAGE_LOADED);
}
// 如果鎖已經被其他執行緒持有,則會阻塞,當其他的執行緒執行完畢後會釋放該鎖,此時在等待的執行緒會獲得該所繼續向下面執行
loadFromUriLock.lock();
Bitmap bmp;
try {
if (checkTaskIsNotActual()) return;
// 先從記憶體中查詢
bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp == null) {
// 在硬碟中查詢,再從網路中查詢圖片
bmp = tryLoadBitmap();
if (bmp == null) return;
.......
if (bmp != null && options.isCacheInMemory()) {
log(LOG_CACHE_IMAGE_IN_MEMORY);
configuration.memoryCache.put(memoryCacheKey, bmp);
}
} else {
log(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING);
}
........
} finally {
loadFromUriLock.unlock();
}
if (checkTaskIsNotActual() || checkTaskIsInterrupted()) return;
// 有四種策略可以顯示圖片
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine);
displayBitmapTask.setLoggingEnabled(loggingEnabled);
handler.post(displayBitmapTask);
}
大家應該注意到有這樣的程式碼ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;都知道這是一個鎖機制,為什麼這裡會出現鎖呢?在載入圖片之前會判斷loadFromUriLock.isLocked()是否被上鎖了,如果上鎖了也就意味著同一個uri對應的圖片載入任務已經在執行了,大家可以想象一下這個場景,在listview中當你快速上下滑動列表,同一個uri對對應的圖片是否應該被載入多次呢,所以這裡當第二次載入同樣的uri的時候這裡通過判斷loadFromUriLock.isLocked()返回true,執行這行程式碼loadFromUriLock.lock();的時候就會造成堵塞,當這個uri對應的第一個載入任務執行完畢後,這個鎖是會釋放掉的,所以後面的任務往下執行,第一個任務執行完畢後,是會把圖片放入快取中,所以之後的任務就會再從內粗快取中查詢是否有uri對應的圖片,至此,已經從記憶體快取中查找了兩次。
如果是第一次載入這個uri,那麼兩次查詢快取肯定都是空的,那麼就要從檔案和網路中查找了,所以會執行 tryLoadBitmap();方法,載入完畢之後會根據指定的圖片顯示策略顯示圖片。
我們重點關注一下tryLoadBitmap這個方法
<span style="white-space:pre"> </span>private Bitmap tryLoadBitmap() {
// 硬碟快取中查詢檔案
File imageFile = getImageFileInDiscCache();
Bitmap bitmap = null;
try {
if (imageFile.exists()) {
// 硬碟快取中存在
log(LOG_LOAD_IMAGE_FROM_DISC_CACHE);
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
}
if (bitmap == null) {
log(LOG_LOAD_IMAGE_FROM_NETWORK);
String imageUriForDecoding = options.isCacheOnDisc() ? tryCacheImageOnDisc(imageFile) : uri;
// 根據uri中指定的協議從何處載入圖片,http、assert、file等協議
bitmap = decodeImage(imageUriForDecoding);
if (bitmap == null) {
fireImageLoadingFailedEvent(FailType.DECODING_ERROR, null);
}
}
} catch (IllegalStateException e) {
........
}
return bitmap;
}
首先根據uri從檔案中查詢,存在就直接解碼圖片顯示,不存在的話就要根據指定的協議去載入,這個協議可以使http、assert、file等,關注的方法是BaseImageDecoder類中的decode方法:
<span style="white-space:pre"> </span>public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
// 返回代表圖片的輸入流,這裡也有幾種策略,緩慢網路、基本下載器等策略
InputStream imageStream = getImageStream(decodingInfo);
ImageFileInfo imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo.getImageUri());
Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
imageStream = getImageStream(decodingInfo);
Bitmap decodedBitmap = decodeStream(imageStream, decodingOptions);
if (decodedBitmap == null) {
L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
} else {
decodedBitmap = considerExactScaleAndOrientaiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation, imageInfo.exif.flipHorizontal);
}
return decodedBitmap;
}
<span style="white-space:pre"> </span>protected InputStream getImageStream(ImageDecodingInfo decodingInfo) throws IOException {
<span style="white-space:pre"> </span>return decodingInfo.getDownloader().getStream(decodingInfo.getImageUri(), decodingInfo.getExtraForDownloader());
<span style="white-space:pre"> </span>}
getDownLoader()獲取下載器可能會返回幾種下載器,一個是SlowNetworkImageDownloader載入器、NetworkDeniedImageDownloader載入器、HttpClientImageDownloader載入器。HttpClientImageDownloader下載器中使用的是HttpGet請求網路。我們重點關注的是SlowNetworkImageDownloader載入器,SlowNetworkImageDownloader原型如下:
<span style="white-space:pre"> </span>@Override
public InputStream getStream(String imageUri, Object extra) throws IOException {
InputStream imageStream = wrappedDownloader.getStream(imageUri, extra);
switch (Scheme.ofUri(imageUri)) {
case HTTP:
case HTTPS:
return new FlushedInputStream(imageStream);
default:
return imageStream;
}
}
是通過FlushedInputStream來獲取流資料的:
<span style="white-space:pre"> </span>public class FlushedInputStream extends FilterInputStream {
<span style="white-space:pre"> </span>public FlushedInputStream(InputStream inputStream) {
<span style="white-space:pre"> </span>super(inputStream);
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>@Override
<span style="white-space:pre"> </span>public long skip(long n) throws IOException {
<span style="white-space:pre"> </span>long totalBytesSkipped = 0L;
<span style="white-space:pre"> </span>while (totalBytesSkipped < n) {
<span style="white-space:pre"> </span>long bytesSkipped = in.skip(n - totalBytesSkipped);
<span style="white-space:pre"> </span>if (bytesSkipped == 0L) {
<span style="white-space:pre"> </span>int by_te = read();
<span style="white-space:pre"> </span>if (by_te < 0) {
<span style="white-space:pre"> </span>break; // we reached EOF
<span style="white-space:pre"> </span>} else {
<span style="white-space:pre"> </span>bytesSkipped = 1; // we read one byte
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>totalBytesSkipped += bytesSkipped;
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>return totalBytesSkipped;
<span style="white-space:pre"> </span>}
}
為什麼使用FlushedInputStream呢?大家想想以前你們是怎麼請求網路圖片的,一般是通過http請求,請求完後使用BitmapFactory的decodeStream方法來獲得一個bitmap。但是這個方法有個致命的bug就是在網路很慢的請看下面會無法獲取完整的資料,從而導致imageview失真或者顯示出問題,處理這個問題我們可以繼承FilterInputStream來處理skip方法強制實現flush流中的資料。主要原理就是檢查檔案是否到檔案末端,告訴http是否需要繼續請求。
上述步驟執行完畢後,一個圖片的資料正常獲取,講該圖片放入快取中,釋放鎖。