Android 開源架構Universal-Image-Loader完全解析(三)---原始碼解讀

來源:互聯網
上載者:User

標籤:

轉載請註明本文出自xiaanming的部落格(http://blog.csdn.net/xiaanming/article/details/39057201),請尊重他人的辛勤勞動成果,謝謝!

本篇文章主要是帶大家從源碼的角度上面去解讀這個強大的圖片載入架構,自己很久沒有寫文章了,感覺生疏了許多,距離上一篇文章三個月多了,確實是自己平常忙,換了工作很多東西都要去看去理解,然後加上自己也懶了,沒有以前那麼有激情了,我感覺這節奏不對,我要繼續保持以前的激情,正所謂好記性不如爛筆頭,有時候自己也會去翻看下之前寫的東西,我覺得知識寫下來比在腦海中留存的更久,今天就給大家來讀一讀這個架構的源碼,我感覺這個圖片載入架構確實寫的很不錯,讀完代碼自己也學到了很多。我希望大家可以先去看下Android 開源架構Universal-Image-Loader完全解析(一)--- 基本介紹及使用, Android 開源架構Universal-Image-Loader完全解析(二)--- 圖片緩衝策略詳解 ,我希望大家可以堅持看完,看完了對你絕對是有收穫的。

ImageView mImageView = (ImageView) findViewById(R.id.image);            String imageUrl = "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s1024/A%252520Photographer.jpg";                        //顯示圖片的配置            DisplayImageOptions options = new DisplayImageOptions.Builder()                    .showImageOnLoading(R.drawable.ic_stub)                    .showImageOnFail(R.drawable.ic_error)                    .cacheInMemory(true)                    .cacheOnDisk(true)                    .bitmapConfig(Bitmap.Config.RGB_565)                    .build();                        ImageLoader.getInstance().displayImage(imageUrl, mImageView, options);    

  大部分的時候我們都是使用上面的代碼去載入圖片,我們先看下

public void displayImage(String uri, ImageView imageView, DisplayImageOptions options) {          displayImage(uri, new ImageViewAware(imageView), options, null, null);      }  

  從上面的代碼中,我們可以看出,它會將ImageView轉換成ImageViewAware, ImageViewAware主要是做什麼的呢?該類主要是將ImageView進行一個封裝,將ImageView的強引用變成弱引用,當記憶體不足的時候,可以更好的回收ImageView對象,還有就是擷取ImageView的寬度和高度。這使得我們可以根據ImageView的寬高去對圖片進行一個裁剪,減少記憶體的使用。

 

接下來看具體的displayImage方法啦,由於這個方法代碼量蠻多的,所以這裡我分開來讀

checkConfiguration();          if (imageAware == null) {              throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);          }          if (listener == null) {              listener = emptyListener;          }          if (options == null) {              options = configuration.defaultDisplayImageOptions;          }            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;          }  

  第1行代碼是檢查ImageLoaderConfiguration是否初始化,這個初始化是在Application中進行的

 

12-21行主要是針對url為空白的時候做的處理,第13行代碼中,ImageLoaderEngine中存在一個HashMap,用來記錄正在載入的任務,載入圖片的時候會將ImageView的id和圖片的url加上尺寸加入到HashMap中,載入完成之後會將其移除,然後將DisplayImageOptions的imageResForEmptyUri的圖片設定給ImageView,最後回調給ImageLoadingListener介面告訴它這次任務完成了。

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

  第1行主要是將ImageView的寬高封裝成ImageSize對象,如果擷取ImageView的寬高為0,就會使用手機螢幕的寬高作為ImageView的寬高,我們在使用ListView,GridView去載入圖片的時候,第一頁擷取寬度是0,所以第一頁使用的手機的螢幕寬高,後面的擷取的都是控制項本身的大小了

 

第7行從記憶體緩衝中擷取Bitmap對象,我們可以再ImageLoaderConfiguration中配置記憶體緩衝邏輯,預設使用的是LruMemoryCache,這個類我在前面的文章中講過

第11行中有一個判斷,我們如果在DisplayImageOptions中設定了postProcessor就進入true邏輯,不過預設postProcessor是為null的,BitmapProcessor介面主要是對Bitmap進行處理,這個架構並沒有給出相對應的實現,如果我們有自己的需求的時候可以自己實現BitmapProcessor介面(比如將圖片設定成圓形的)

第22 -23行是將Bitmap設定到ImageView上面,這裡我們可以在DisplayImageOptions中配置顯示需求displayer,預設使用的是SimpleBitmapDisplayer,直接將Bitmap設定到ImageView上面,我們可以配置其他的顯示邏輯, 他這裡提供了FadeInBitmapDisplayer(透明度從0-1)RoundedBitmapDisplayer(4個角是圓弧)等, 然後回調到ImageLoadingListener介面

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

  這段代碼主要是Bitmap不在記憶體緩衝,從檔案中或者網路裡面擷取bitmap對象,執行個體化一個LoadAndDisplayImageTask對象,LoadAndDisplayImageTask實現了Runnable,如果配置了isSyncLoading為true, 直接執行LoadAndDisplayImageTask的run方法,表示同步,預設是false,將LoadAndDisplayImageTask提交給線程池對象

 

接下來我們就看LoadAndDisplayImageTask的run(), 這個類還是蠻複雜的,我們還是一段一段的分析

if (waitIfPaused()) return;  if (delayIfNeed()) return;  

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

private boolean waitIfPaused() {      AtomicBoolean pause = engine.getPause();      if (pause.get()) {          synchronized (engine.getPauseLock()) {              if (pause.get()) {                  L.d(LOG_WAITING_FOR_RESUME, memoryCacheKey);                  try {                      engine.getPauseLock().wait();                  } catch (InterruptedException e) {                      L.e(LOG_TASK_INTERRUPTED, memoryCacheKey);                      return true;                  }                  L.d(LOG_RESUME_AFTER_PAUSE, memoryCacheKey);              }          }      }      return isTaskNotActual();  }  

  這個方法是幹嘛用呢,主要是我們在使用ListView,GridView去載入圖片的時候,有時候為了滑動更加的流暢,我們會選擇手指在滑動或者猛地一滑動的時候不去載入圖片,所以才提出了這麼一個方法,那麼要怎麼用呢?  這裡用到了PauseOnScrollListener這個類,使用很簡單ListView.setOnScrollListener(new PauseOnScrollListener(pauseOnScroll, pauseOnFling )), pauseOnScroll控制我們緩慢滑動ListView,GridView是否停止載入圖片,pauseOnFling 控制猛的滑動ListView,GridView是否停止載入圖片

除此之外,這個方法的傳回值由isTaskNotActual()決定,我們接著看看isTaskNotActual()的源碼

private boolean isTaskNotActual() {          return isViewCollected() || isViewReused();      }  

  isViewCollected()是判斷我們ImageView是否被記憶體回收行程回收了,如果回收了,LoadAndDisplayImageTask方法的run()就直接返回了,isViewReused()判斷該ImageView是否被重用,被重用run()方法也直接返回,為什麼要用isViewReused()方法呢?主要是ListView,GridView我們會複用item對象,假如我們先去載入ListView,GridView第一頁的圖片的時候,第一頁圖片還沒有全部載入完我們就快速的滾動,isViewReused()方法就會避免這些不可見的item去載入圖片,而直接載入當前介面的圖片

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

  第1行代碼有一個loadFromUriLock,這個是一個鎖,擷取鎖的方法在ImageLoaderEngine類的getLockForUri()方法中

ReentrantLock getLockForUri(String uri) {          ReentrantLock lock = uriLocks.get(uri);          if (lock == null) {              lock = new ReentrantLock();              uriLocks.put(uri, lock);          }          return lock;      }  

  從上面可以看出,這個鎖對象與圖片的url是相互對應的,為什麼要這麼做?也行你還有點不理解,不知道大家有沒有考慮過一個情境,假如在一個ListView中,某個item正在擷取圖片的過程中,而此時我們將這個item滾出介面之後又將其滾進來,滾進來之後如果沒有加鎖,該item又會去載入一次圖片,假設在很短的時間內滾動很頻繁,那麼就會出現多次去網路上面請求圖片,所以這雷根據圖片的Url去對應一個ReentrantLock對象,讓具有相同Url的請求就會在第7行等待,等到這次圖片載入完成之後,ReentrantLock就被釋放,剛剛那些相同Url的請求就會繼續執行第7行下面的代碼

來到第12行,它們會先從記憶體緩衝中擷取一遍,如果記憶體緩衝中沒有在去執行下面的邏輯,所以ReentrantLock的作用就是避免這種情況下重複的去從網路上面請求圖片。

第14行的方法tryLoadBitmap(),這個方法確實也有點長,我先告訴大家,這裡面的邏輯是先從檔案快取中擷取有沒有Bitmap對象,如果沒有在去從網路中擷取,然後將bitmap儲存在檔案系統中,我們還是具體分析下

File imageFile = configuration.diskCache.get(uri);              if (imageFile != null && imageFile.exists()) {                  L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);                  loadedFrom = LoadedFrom.DISC_CACHE;                    checkTaskNotActual();                  bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));              }  

  先判斷檔案快取中有沒有該檔案,如果有的話,直接去調用decodeImage()方法去解碼圖片,該方法裡面調用BaseImageDecoder類的decode()方法,根據ImageView的寬高,ScaleType去裁剪圖片,具體的代碼我就不介紹了,大家自己去看看,我們接下往下看tryLoadBitmap()方法

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);                if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {                  fireFailEvent(FailType.DECODING_ERROR, null);              }          }  

  第1行表示從檔案快取中擷取的Bitmap為null,或者寬高為0,就去網路上面擷取Bitmap,來到第6行代碼是否配置了DisplayImageOptions的isCacheOnDisk,表示是否需要將Bitmap對象儲存在檔案系統中,一般我們需要配置為true, 預設是false這個要注意下,然後就是執行tryCacheImageOnDisk()方法,去伺服器上面拉取圖片並儲存在本地檔案中

private Bitmap decodeImage(String imageUri) throws IOException {      ViewScaleType viewScaleType = imageAware.getScaleType();      ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,              getDownloader(), options);      return decoder.decode(decodingInfo);  }    /** @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());      return configuration.diskCache.save(uri, is, this);  }  

  第6行的downloadImage()方法是負責下載圖片,並將其保持到檔案快取中,將下載儲存Bitmap的進度回調到IoUtils.CopyListener介面的onBytesCopied(int current, int total)方法中,所以我們可以設定ImageLoadingProgressListener介面來擷取圖片下載儲存的進度,這裡儲存在檔案系統中的圖片是原圖

第16-17行,擷取ImageLoaderConfiguration是否設定儲存在檔案系統中的圖片大小,如果設定了maxImageWidthForDiskCache和maxImageHeightForDiskCache,會調用resizeAndSaveImage()方法對圖片進行裁剪然後在替換之前的原圖,儲存裁剪後的圖片到檔案系統的,之前有同學問過我說這個架構儲存在檔案系統的圖片都是原圖,怎麼才能儲存縮圖,只要在Application中執行個體化ImageLoaderConfiguration的時候設定maxImageWidthForDiskCache和maxImageHeightForDiskCache就行了

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

  接下來這裡就簡單了,6-12行是否要對Bitmap進行處理,這個需要自行實現,14-17就是將圖片儲存到記憶體緩衝中去

DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);          runTask(displayBitmapTask, syncLoading, handler, engine);  

  最後這兩行代碼就是一個顯示任務,直接看DisplayBitmapTask類的run()方法

 public void run() {          if (imageAware.isCollected()) {              L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);              listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());          } else if (isViewWasReused()) {              L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);              listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());          } else {              L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey);              displayer.display(bitmap, imageAware, loadedFrom);              engine.cancelDisplayTaskFor(imageAware);              listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);          }      }  

  

假如ImageView被回收了或者被重用了,回調給ImageLoadingListener介面,否則就調用BitmapDisplayer去顯示Bitmap

文章寫到這裡就已經寫完了,不知道大家對這個開源架構有沒有進一步的理解,這個開源架構設計也很靈活,用了很多的設計模式,比如建造者模式,裝飾模式,代理模式,策略模式等等,這樣方便我們去擴充,實現我們想要的功能,今天的講解就到這了,有對這個架構不明白的地方可以在下面留言,我會盡量為大家解答的。

 

Android 開源架構Universal-Image-Loader完全解析(三)---原始碼解讀

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.