Tips:文章爲拜讀@CodingForAndroid 後有感而作的分享,先對做者表示感謝,附原文地址:http://blog.csdn.net/u011733020android
在Android開發中,對於圖片的加載能夠說是個老生常談的問題了,圖片加載是一個比較坑的地方,處理很差,會有各類奇怪的問題,好比 加載致使界面卡頓,程序crash。數據庫
所以 如何高效的加載大量圖片,以及如何加載大分辨率的圖片到內存,是咱們想要開發一款優質app時不得不去面對與解決的問題。緩存
一般開發中,咱們只有兩種選擇:安全
使用開源框架網絡
本身去實現處理圖片的加載與緩存。app
一般一開始讓咱們本身去寫,咱們會無從下手,所以先去分析一下開源的思路,對咱們的成長頗有必要。框架
目前使用頻率較高的圖片緩存框架有 Universal-Image-Loader、android-Volley、Picasso、Fresco和Glide五大Android開源組件。ide
首先排除android-Volley 孰優孰劣,後面再去驗證,剩下的四種對於 圖片加載緩存的思想,從大方向上應該是相似的工具
而Android-Universal-Image-Loader 做爲一款比較經典的框架,從早期到如今一直都比較常見,這裏就拿Android-Universal-Image-Loader 來看一下它對圖片處理的思想,以幫助咱們理解,以便於咱們也能寫出相似的框架。url
前面介紹瞭如何在咱們的項目中使用Android-Universal-Image-Loader,本文看一下UIL的工做過程。
在看以前咱們先看一下官方的這張圖片,它表明着全部條件下的執行流程:
圖片給出的加載過程分別對應三種狀況:
當內存中有該 bitmap 時,直接顯示。
當本地有該圖片時,加載進內存,而後顯示。
內存本地都沒有時,請求網絡,下載到本地,接下來加載進內存,而後顯示。
* 過程分析:
最終展現圖片仍是調用的 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設置爲背景。
這就是大致工做流程,也是前面說的的 三種狀況
內存中有,直接顯示。
內存中沒有 本地有,加載進內存並顯示。
本地沒有,網絡下載,本地保存,加載進內存,顯示。
接下來再看前面說的下載方法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) 的兩級緩存策略。
採用緩存的好處有如下幾點:
減小每次請求網絡下載消耗的的流量。
複用直接從內存/本地中獲取,提升了加載速度。
那麼咱們接下來看一下UIL 是採起哪些方式去緩存內存和本地文件的。
經過查看UIL的lib庫咱們能夠看出,整個lib 主要有三個包組成
cache:管理緩存
core:下載的核心
utils:一些輔助工具。
utils 不用管,剩下的兩部分就是整個項目的精髓: 下載展現 和緩存。
咱們這裏先看一下cache:
也是由兩部分組成:磁盤和內存。
disc 有兩種cache類型:
第一類是是基於DiskLruCache的LruDiskCache
第二類是基於BaseDiskCache的LimitedAgeDiskCache 和UnlimitedDiskCache 。
這兩種的相同點是都是將請求到的圖片 inputStream寫到本地文件中。不一樣點在魚管理方式不一樣,
LruDiskCache是根據 size > maxSize || fileCount > maxFileCount || 或者存的數據超過2000條而自動去刪除。
LimitedAgeDiskCache 是根據存入時間與當前時間差,是否大於過時時間 去判斷是重新下載 仍是重複利用。
UnlimitedDiskCache:這個就是不限制cache大小,只要disk 上有空間 就能夠保存到本地。
以上三個都實現了DiskCache 這個接口,具體工做過程是 save get remove clear 等幾個方法,相似於數據庫的 curd 操做。
memory的緩存 的實現類比較多,都是實現了 MemoryCache 這個接口
public interface MemoryCache {
/** Puts value into cache by key 根據Key將Value添加進緩存中 @return <b>true</b> - if value was put into cache successfully, <b>false</b> - if value was <b>not</b> 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<String> 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<Bitmap>(value)中,而後在加入Map<k v> 中 // Add value to soft cache super.put(key, value); return putSuccessfully; }
註釋的很詳細, 首先判斷大小能夠加入List<Bitmap> hardCache 這樣一個集合中, 若是能夠則加入在判斷 當前集合是否超出 設置的默認最大值,若是該圖片不能加入到這個集合中,那麼首先將value 添加到WeakReference<Bitmap>(value)中,而後將WeakReference 做爲value 添加到另外一個Map 中保存。
前面看上去比較很差理解,第一遍看可能會以爲很亂,這裏在總結一下加載的過程: