Android-Universal-Image-Loader 源碼解讀

Android-Universal-Image-Loader 源碼解讀

Tips:文章爲拜讀@CodingForAndroid 後有感而作的分享,先對做者表示感謝,附原文地址:http://blog.csdn.net/u011733020android

0 前言

  • 在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

1 工做流程

  • 前面介紹瞭如何在咱們的項目中使用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 ,並返回 用於顯示。

2 緩存策略分析

  • 經過上圖,咱們能夠總結出 UIL採用的是 內存(memory cache)+本地(disk cache) 的兩級緩存策略。

  • 採用緩存的好處有如下幾點:

    1. 減小每次請求網絡下載消耗的的流量。

    2. 複用直接從內存/本地中獲取,提升了加載速度。

  • 那麼咱們接下來看一下UIL 是採起哪些方式去緩存內存和本地文件的。

  • 經過查看UIL的lib庫咱們能夠看出,整個lib 主要有三個包組成

  1. cache:管理緩存

  2. core:下載的核心

  3. 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 <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 中保存。

3 總結

  • 前面看上去比較很差理解,第一遍看可能會以爲很亂,這裏在總結一下加載的過程:

相關文章
相關標籤/搜索