1. 程式人生 > >Android-UIL圖片快取框架 原始碼解析

Android-UIL圖片快取框架 原始碼解析

Android-Universal-Image-Loader 是 github上一個開源的圖片快取框架 ,提供圖片MemoryCache和DiskCache的功能,並支援載入網路、本地、contentProvider圖片的功能

Acceptable URIs examples

"http://site.com/image.png" // from Web
"file:///mnt/sdcard/image.png" // from SD card
"file:///mnt/sdcard/video.mp4" // from SD card (video thumbnail)
"content://media/external/images/media/13"
// from content provider "content://media/external/video/media/13" // from content provider (video thumbnail) "assets://image.png" // from assets "drawable://" + R.drawable.img // from drawables (non-9patch images) //通常不用。

NOTE: Use drawable:// only if you really need it! Always consider the native way to load drawables -ImageView.setImageResource(...)

 instead of using of ImageLoader.

下面我來從原始碼的角度分析一下這個開源專案的流程:

首先 先寫一個簡單的例子:

 ImageLoader imageLoader = ImageLoader.getInstance();
        ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this).build();

        imageLoader.init(config);
        imageLoader.displayImage("http://pic32.nipic.com/20130829/12906030_124355855000_2.png", image);
第一行 要先例項化ImageLoader  採用了單例模式例項化  

然後需要給imageLoader 初始化配置資訊,也就是ImageLoaderConfiguration 這個類  如果不初始化 會報異常  

接下來我們來看看這個類中都可以初始化哪些變數:

final Resources resources; //用於載入app中資原始檔

	final int maxImageWidthForMemoryCache;  //記憶體快取的圖片寬度最大值  預設為螢幕寬度
	final int maxImageHeightForMemoryCache;  //同上
	final int maxImageWidthForDiskCache;  //磁碟快取寬度  預設無限制
	final int maxImageHeightForDiskCache;  //同上
	final BitmapProcessor processorForDiskCache;  //點陣圖處理器  磁碟快取 處理器

	final Executor taskExecutor;   //任務執行者
	final Executor taskExecutorForCachedImages;   //快取圖片任務執行者
	final boolean customExecutor;   //自定義的任務執行者
	final boolean customExecutorForCachedImages;   //自定義的快取圖片任務執行者

	final int threadPoolSize;   //執行緒池 大小  預設為3
	final int threadPriority;  //執行緒優先順序
	final QueueProcessingType tasksProcessingType;   //佇列的型別 可以選擇 FIFO(先進先出)LIFO(後進先出)

	final MemoryCache memoryCache;   //記憶體快取
	final DiskCache diskCache;   //磁碟快取
	final ImageDownloader downloader;  //圖片下載器
	final ImageDecoder decoder;    //圖片解碼器
	final DisplayImageOptions defaultDisplayImageOptions;  //圖片展示選項

	final ImageDownloader networkDeniedDownloader;   //離線圖片下載器
	final ImageDownloader slowNetworkDownloader;   //網速慢圖片下載器
在這個配置類中可以初始化以上內容  下面是一些預設的初始化  
File cacheDir = StorageUtils.getCacheDirectory(context);  
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)  
        .memoryCacheExtraOptions(480, 800) // default = device screen dimensions  
        .diskCacheExtraOptions(480, 800, CompressFormat.JPEG, 75, null)  
        .taskExecutor(...)  
        .taskExecutorForCachedImages(...)  
        .threadPoolSize(3) // default  
        .threadPriority(Thread.NORM_PRIORITY - 1) // default  
        .tasksProcessingOrder(QueueProcessingType.FIFO) // default  
        .denyCacheImageMultipleSizesInMemory()  
        .memoryCache(new LruMemoryCache(2 * 1024 * 1024))  
        .memoryCacheSize(2 * 1024 * 1024)  
        .memoryCacheSizePercentage(13) // default  
        .diskCache(new UnlimitedDiscCache(cacheDir)) // default  
        .diskCacheSize(50 * 1024 * 1024)  
        .diskCacheFileCount(100)  
        .diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) // default  
        .imageDownloader(new BaseImageDownloader(context)) // default  
        .imageDecoder(new BaseImageDecoder()) // default  
        .defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default  
        .writeDebugLogs()  
        .build();  


可以根據自己的需要選擇需要使用的disk和memory快取策略

接下來我們繼續往下看:

public synchronized void init(ImageLoaderConfiguration configuration) {
		if (configuration == null) {
			throw new IllegalArgumentException(ERROR_INIT_CONFIG_WITH_NULL);
		}
		if (this.configuration == null) {
			L.d(LOG_INIT_CONFIG);
			engine = new ImageLoaderEngine(configuration);
			this.configuration = configuration;
		} else {
			L.w(WARNING_RE_INIT_CONFIG);
		}
	}
init方法  傳入配置資訊  並根據配置資訊初始化 ImageLoaderEngine引擎類  (主要是 初始化其中的TaskExecutor)  

之後 便是 displayImage方法了   

下面我們來看 displayImage這個方法 

這個方法  引數最多的過載是 :

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) 

引數包括  圖片uri   圖片控制元件  展示圖片的選項、影象的大小 、影象載入的監聽、影象載入的進度條監聽等  其中 options中還可以設定更多的選項

下面正式開始看 displayImage方法的原始碼  (由於太長 一步步來看):

<span style="white-space:pre">		</span>checkConfiguration();  
		if (imageAware == null) {
			throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
		}
		if (listener == null) {
			listener = defaultListener;
		}
		if (options == null) {
			options = configuration.defaultDisplayImageOptions;
		}

第一行 很簡單  檢查 是否有配置檔案  

private void checkConfiguration() {
if (configuration == null) {
throw new IllegalStateException(ERROR_NOT_INIT);
}
}

下面幾行 也是類似  如果所判斷的變數為空則初始化一個  

<span style="white-space:pre">		</span>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) {
			targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
		}

繼續,第一行  判斷uri是否是空  如果是空 直接取消engine中的任務 (此處我覺得也還沒在engine中新增任務呀,為什麼要remove 但是remove肯定沒錯 那麼就先remove著吧。)

然後呼叫listener的start 之後由於 uri為空  如果設定了需要設定空的影象那麼直接設定 影象是  空的時候需要設定的影象即可 如果沒設定,直接不顯示就好 

之後呼叫 complete 回撥  返回   這是uri為空的情況  不需要做太多操作 也不需要快取  

如果 影象的大小 設定是空 那麼根據控制元件設定的大小  設定 要展示圖片的大小  

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);
}

String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
		engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);

		listener.onLoadingStarted(uri, imageAware.getWrappedView());
之後 根據 uri和目標的大小  生成一個key 並把 這個任務放入 engine 的集合中

回撥 started方法  

<span style="white-space:pre">		</span>Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);  //從記憶體快取取
		if (bmp != null && !bmp.isRecycled()) {  //如果存在 並且沒被回收
			L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);

				if (options.shouldPostProcess()) { //如果設定了 postProcess 執行  預設沒設定  設定這個可以提前對圖片進行某些處理
				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);
			}
		}

接下來是重要的部分 

首先第一行 從記憶體快取中根據key取bitmap  

第二行 判斷 記憶體中有沒有和 有沒有被記憶體回收   如果存在切沒被回收 那麼就比較簡單了  

先對圖片進行一些處理  然後把圖片展示出來即可
其中 上述的那幾行  task 程式碼  的主要目的就是 封裝了一些在展示圖片之前的一些對圖片的處理 然後再展示圖片  

倒數第五行的else 語句  是在  不需要 在展示圖片之前處理圖片時,那麼就直接使用 displaywe 對 圖片進行 展示  並回調complete函式 

其中 這個displayer可以設定 fadeIn(透明度) 和 Circle  displayer(圓角)   看自己需要了

 public void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom) {
imageAware.setImageBitmap(bitmap);
}

  普通的displayer   display非常簡單 見上面程式碼  

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()) { //表示同步  直接執行
<span style="white-space:pre">				</span>displayTask.run();
<span style="white-space:pre">			</span>} else {  // 不同步 那麼就交給執行緒池 物件執行  engine中 有  Executor  這其中有 執行緒池
<span style="white-space:pre">				</span>engine.submit(displayTask);
<span style="white-space:pre">			</span>}

		}
繼續 原始碼   如果  不在記憶體快取中 那麼 就麻煩了  大體的操作步驟是 先從圖片原始地載入圖片,得到圖片後放入硬碟和記憶體  然後展示  

第二行  如果載入時需要顯示圖片  那麼設定  否則 不設定圖片  

然後 設定正在載入時的資訊  ImageLoadingInfo   和 任務LoadAndDisplayImageTask   

之後根據是否同步  執行任務  

接下來看  displayTask的run方法  

<span style="white-space:pre">		</span>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();
前兩行是 

如果waitIfPaused(), delayIfNeed()返回true的話,直接從run()方法中返回了,不執行下面的邏輯,

這兩個方法  主要是判斷是否是被中斷了任務 或者要延時任務的  

繼續看 第四行 獲取了 一個鎖  然後 給其 加鎖  這是為了防止重複的載入

假如在一個ListView中,某個item正在獲取圖片的過程中,而此時我們將這個item滾出介面之後又將其滾進來,滾進來之後如果沒有加鎖,該item又會去載入一次圖片,假設在很短的時間內滾動很頻繁,那麼就會出現多次去網路上面請求圖片,所以這裡根據圖片的Url去對應一個ReentrantLock物件,讓具有相同Url的請求就會在最後一行等待,等到這次圖片載入完成之後,ReentrantLock就被釋放,剛剛那些相同Url的請求就會繼續執行下面的程式碼

<span style="white-space:pre">		</span>Bitmap bmp;
		try {
			checkTaskNotActual(); //檢查任務是否還在 

			bmp = configuration.memoryCache.get(memoryCacheKey);  //從記憶體快取獲取bmp
			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 { //記憶體快取中有  設定 from 為  記憶體快取
				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();  //釋放鎖
		}
        //下面兩行是顯示圖片的任務 上面是載入bitmap  現已載入好 並快取到 記憶體和磁碟中  只需要顯示即可
		//回撥介面的  oncancle  和 oncomplete方法 在這裡呼叫   進度條的 在 從網路獲取的時候回撥  onstart在最開始回撥
		DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
		runTask(displayBitmapTask, syncLoading, handler, engine);

上面的程式碼 就是 快取部分的了   首先從記憶體中根據key讀取  如果記憶體中沒有 或者說 已經被 回收了  那麼就執行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()));  //直接解析出bitmap
			}
			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()) {  //tryCahcheImageDisk 方法 從網路下載 並快取到硬碟
					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這個方法的作用 是 在磁碟中未取到 時  從網路獲取圖片 並快取到磁碟中去  
/** @return <b>true</b> - if image was downloaded successfully; <b>false</b> - otherwise */
	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);
			}
		}
	}
這樣 就完成了  圖片的 快取 與顯示