Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android-Universal-Image-Loader (圖片異步加載緩存庫)的源碼解讀

Android-Universal-Image-Loader (圖片異步加載緩存庫)的源碼解讀

編輯:關於Android編程

前言:

在Android開發中,對於圖片的加載可以說是個老生常談的問題了,圖片加載是一個比較坑的地方,處理不好,會有各種奇怪的問題,比如 加載導致界面卡頓,程序crash。
因此 如何高效的加載大量圖片,以及如何加載大分辨率的圖片到內存,是我們想要開發一款優質app時不得不去面對與解決的問題。
通常開發中,我們只有兩種選擇:① 使用開源框架 ②自己去實現處理圖片的加載與緩存。
通常一開始讓我們自己去寫,我們會無從下手,因此先去分析一下開源的思路,對我們的成長很有必要。
目前使用頻率較高的圖片緩存框架有 Universal-Image-Loader、android-Volley、Picasso、Fresco和Glide五大Android開源組件。
首先排除android-Volley 孰優孰劣,後面再去驗證,剩下的四種對於 圖片加載緩存的思想,從大方向上應該是類似的
而Android-Universal-Image-Loader 作為一款比較經典的框架,從早期到現在一直都比較常見,這裡就拿Android-Universal-Image-Loader 來看一下它對圖片處理的思想,以幫助我們理解,以便於我們也能寫出類似的框架。

關於使用配置請看Android-Universal-Image-Loader (圖片異步加載緩存庫)的使用配置

正文:

 

一 工作流程

前面介紹了如何在我們的項目中使用Android-Universal-Image-Loader,本文看一下UIL的工作過程。

在看之前我們先看一下官方的這張圖片,它代表著所有條件下的執行流程:

\

圖片給出的加載過程分別對應三種情況:

1.當內存中有該 bitmap 時,直接顯示。

2.當本地有該圖片時,加載進內存,然後顯示。

3. 內存本地都沒有時,請求網絡,下載到本地,接下來加載進內存,然後顯示。

 

過程分析:

 

最終展示圖片還是調用的ImageLoader 這個類中的 display() 方法,那麼我們就把注意力集中到ImageLoader 這個類上,看下display()內部怎麼實現的。

 

	public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
			ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
		// 首先檢查初始化配置,configuration == null 拋出異常
		checkConfiguration();
		if (imageAware == null) {
			throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
		}
		if (listener == null) {
			listener = defaultListener;
		}
		if (options == null) {
			options = configuration.defaultDisplayImageOptions;
		}
		// 當  目標uri "" 時這種情況的處理
		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;
		}
		// 根據  配置的大小與圖片實際大小得出 圖片尺寸
		ImageSize 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);
			}
		} 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);
			}
		}
	}

可以看到這個方法還不長,大體流程也向前面圖中描述的:

 

首先判斷傳入的目標url 是" ",如果空,是否配置了默認的圖片,接下來重點在url 是合法的情況下,去加載bitmap,首先從內存中去取,看能否取到(如果前面加載到內存,並且緩存過,沒有被移除,則可以取到),如果取到則直接展示就可以了。

 

如果沒有在內存中取到,接下來執行LoadAndDisplayImageTask 這個任務,主要還是看run()方法的執行過程:

 

@Override
	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()) { 
				// cache 中沒有,下載
				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);
				 //LruMemoryCache
					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);
	}

這裡一開始進行了一些基本的判斷,比如是否當前暫停加載,延時加載等情況。

 

接下來,因為設置到了下載讀寫等過程,所以加了 鎖,保證線程安全,下載過程是在 上面的// cache 中沒有,這個注釋下面的tryLoadBitmap() 方法中進行的,這個方法中做了什麼,我們一會在看,現在繼續往下走,下載後拿到了bitmap,接著進行判斷是否 把bitmap加入到內存中的緩存中。 最後在DisplayBitmapTask 的run 方法中setImageBitmap設置為背景。

 

這就是大體工作流程,也是前面說的的 三種情況

1. 內存中有,直接顯示。

2. 內存中沒有 本地有,加載進內存並顯示。

3 本地沒有,網絡下載,本地保存,加載進內存,顯示。

 

接下來再看前面說的下載方法tryLoadBitmap(), 由於比較長,這裡只看關鍵的 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);

就是前面的情況,先看本地文件,如果有,加載進內存,並顯示。

 

如果沒有則 下載,首先判斷是否允許保存到本地,如果允許則下載到本地,接下來通過bitmap = decodeImage(imageUriForDecoding);拿到目標bitmap ,並返回 用於顯示。

 

 

二 緩存策略分析



通過上圖,我們可以總結出 UIL采用的是 內存(memory cache)+本地(disk cache) 的兩級緩存策略。
采用緩存的好處有以下幾點:
1. 減少每次請求網絡下載消耗的的流量。
2.復用直接從內存/本地中獲取,提高了加載速度。

 

那麼我們接下來看一下UIL 是采取哪些方式去緩存內存和本地文件的。

通過查看UIL的lib庫我們可以看出,整個lib 主要有三個包組成

\

①cache:管理緩存 ②core:下載的核心 ③utils:一些輔助工具。

utils 不用管,剩下的兩部分就是整個項目的精髓: 下載展示 和緩存。

我們這裡先看一下cache:

\

也是由兩部分組成:磁盤和內存。

DiskCache(本地緩存)

disc 有兩種cache類型:

\

第一類是是基於DiskLruCache的LruDiskCache, 第二類是基於BaseDiskCache的LimitedAgeDiskCache 和UnlimitedDiskCache 。

這兩種的工作原理稍微復雜,在這裡先不做介紹,有時間單獨再專門開篇文章介紹。

這兩種的相同點是都是將請求到的圖片 inputStream寫到本地文件中。不同點在魚管理方式不同,

LruDiskCache是根據size > maxSize || fileCount > maxFileCount || 或者存的數據超過2000條而自動去刪除。

LimitedAgeDiskCache 是根據存入時間與當前時間差,是否大於過期時間 去判斷是從新下載 還是重復利用。

UnlimitedDiskCache:這個就是不限制cache大小,只要disk 上有空間 就可以保存到本地。

以上三個都實現了DiskCache 這個接口,具體工作過程是 save get remove clear 等幾個方法,類似於數據庫的 curd 操作。

 

MemoryCache(內存緩存)

\ memory的緩存 的實現類比較多,都是實現了 MemoryCache 這個接口
public interface MemoryCache {
	/**
	 * Puts value into cache by key
	 *  根據Key將Value添加進緩存中
	 * @return true - if value was put into cache successfully, false - if value was not put into
	 * cache
	 */
	boolean put(String key, Bitmap value);

	/** Returns value by key. If there is no value for key then null will be returned. */
	根據Key 取Value
	Bitmap get(String key);

	/** Removes item by key */
	根據Key移除對應的Value
	Bitmap remove(String key);

	/** Returns all keys of cache */
	返回所有的緩存Keys
	Collection keys();

	/** Remove all items from cache */
	情況緩存的map
	void clear();
}

比較多,不一一說,拿比較常用的LruLimitedMemoryCache 說一下吧
	@Override
	public boolean put(String key, Bitmap value) {
		boolean putSuccessfully = false;
		// Try to add value to hard cache
		// 當前要存入的 size
		int valueSize = getSize(value);
		// 約定的最大size
		int sizeLimit = getSizeLimit();
		//當前存在的size 大小
		int curCacheSize = cacheSize.get();
		//如果當前沒有滿,存入
		if (valueSize < sizeLimit) {
			// 判斷 存入後如果 超出了約定的 maxsize  則刪除掉最早的那一條
			while (curCacheSize + valueSize > sizeLimit) {
				Bitmap removedValue = removeNext();
				if (hardCache.remove(removedValue)) {
					curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
				}
			}
			hardCache.add(value);
			cacheSize.addAndGet(valueSize);

			putSuccessfully = true;
		}
		// 如果過大,則不存入到上面的集合,則將value 先new WeakReference(value)中,然後在加入Map 中
		// Add value to soft cache
		super.put(key, value);
		return putSuccessfully;
	}
注釋的很詳細, 首先判斷大小可以加入List hardCache 這樣一個集合中, 如果可以則加入在判斷 當前集合是否超出 設置的默認最大值,如果該圖片不能加入到這個集合中,那麼首先將value 添加到WeakReference(value)中,然後將WeakReference 作為value 添加到另一個Map 中保存。


總結

前面看上去比較不好理解,第一遍看可能會覺得很亂,這裡在總結一下加載的過程:   \           謝謝認真觀讀本文的每一位小伙伴,衷心歡迎小伙伴給我指出文中的錯誤,也歡迎小伙伴與我交流學習。
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved