Android-Universal-Image-Loader (圖片非同步載入緩衝庫)的源碼解讀
前言:在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()方法的執行過程:
@Overridepublic 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 firedcheckTaskNotActual();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); //LruMemoryCacheconfiguration.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 取ValueBitmap get(String key);/** Removes item by key */根據Key移除對應的ValueBitmap remove(String key);/** Returns all keys of cache */返回所有的緩衝KeysCollection keys();/** Remove all items from cache */情況緩衝的mapvoid clear();}
比較多,不一一說,拿比較常用的LruLimitedMemoryCache 說一下吧
@Overridepublic boolean put(String key, Bitmap value) {boolean putSuccessfully = false;// Try to add value to hard cache// 當前要存入的 sizeint valueSize = getSize(value);// 約定的最大sizeint 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 cachesuper.put(key, value);return putSuccessfully;}注釋的很詳細, 首先判斷大小可以加入List hardCache 這樣一個集合中, 如果可以則加入在判斷 當前集合是否超出 設定的預設最大值,如果該圖片不能加入到這個集合中,那麼首先將value 添加到WeakReference(value)中,然後將WeakReference 作為value 添加到另一個Map 中儲存。
總結前面看上去比較不好理解,第一遍看可能會覺得很亂,這裡在總結一下載入的過程: 謝謝認真觀讀本文的每一位小夥伴,衷心歡迎小夥伴給我指出文中的錯誤,也歡迎小夥伴與我交流學習。