Universal-Image-Loader原始碼解解析---display過程 + 獲取bitmap過程

wangyy發表於2018-03-30

Universal-Image-Loader在github上的地址:https://github.com/nostra13/Android-Universal-Image-Loader

它的基本使用請參考我的另一篇部落格 http://www.cnblogs.com/yuan1225/p/8426900.html,下面我從原始碼角度研究它。

ImageLoader使用的是雙重判斷的懶漢試單例模式。

 1 /** Returns singleton class instance */
 2     public static ImageLoader getInstance() {
 3         if (instance == null) {
 4             synchronized (ImageLoader.class) {
 5                 if (instance == null) {
 6                     instance = new ImageLoader();
 7                 }
 8             }
 9         }
10         return instance;
11     }

 

先看ImageLoader的初始化過程:

 1 /**
 2      * Initializes ImageLoader instance with configuration.<br />
 3      * If configurations was set before ( {@link #isInited()} == true) then this method does nothing.<br />
 4      * To force initialization with new configuration you should {@linkplain #destroy() destroy ImageLoader} at first.
 5      *
 6      * @param configuration {@linkplain ImageLoaderConfiguration ImageLoader configuration}
 7      * @throws IllegalArgumentException if <b>configuration</b> parameter is null
 8      */
 9     public synchronized void init(ImageLoaderConfiguration configuration) {
10         if (configuration == null) {
11             throw new IllegalArgumentException(ERROR_INIT_CONFIG_WITH_NULL);
12         }
13         if (this.configuration == null) {
14             L.d(LOG_INIT_CONFIG);
15             engine = new ImageLoaderEngine(configuration);
16             this.configuration = configuration;
17         } else {
18             L.w(WARNING_RE_INIT_CONFIG);
19         }
20     }

以上是在Application中呼叫 ImageLoader.getInstance().init(config.build());的初始化過程。接下來是ImageLoader的使用過程分析

ImageLoader.getInstance().displayImage(url, imageView, options);
displayImage方法有很大過載的方法,最終都會輾轉呼叫到最複雜的這個過載方法:
 1 public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
 2             ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
 3         checkConfiguration();
 4         if (imageAware == null) {
 5             throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
 6         }
 7         if (listener == null) {
 8             listener = defaultListener;
 9         }
10         if (options == null) {
11             options = configuration.defaultDisplayImageOptions;
12         }
13 
14         if (TextUtils.isEmpty(uri)) {
15             engine.cancelDisplayTaskFor(imageAware);
16             listener.onLoadingStarted(uri, imageAware.getWrappedView());
17             if (options.shouldShowImageForEmptyUri()) {
18                 imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
19             } else {
20                 imageAware.setImageDrawable(null);
21             }
22             listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
23             return;
24         }
25 
26         if (targetSize == null) {
27             targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
28         }
29         String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
30         engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
31 
32         listener.onLoadingStarted(uri, imageAware.getWrappedView());
33 
34         Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
35         if (bmp != null && !bmp.isRecycled()) {
36             L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
37 
38             if (options.shouldPostProcess()) {
39                 ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
40                         options, listener, progressListener, engine.getLockForUri(uri));
41                 ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
42                         defineHandler(options));
43                 if (options.isSyncLoading()) {
44                     displayTask.run();
45                 } else {
46                     engine.submit(displayTask);
47                 }
48             } else {
49                 options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
50                 listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
51             }
52         } else {
53             if (options.shouldShowImageOnLoading()) {
54                 imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
55             } else if (options.isResetViewBeforeLoading()) {
56                 imageAware.setImageDrawable(null);
57             }
58 
59             ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
60                     options, listener, progressListener, engine.getLockForUri(uri));
61             LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
62                     defineHandler(options));
63             if (options.isSyncLoading()) {
64                 displayTask.run();
65             } else {
66                 engine.submit(displayTask);
67             }
68         }
69     }

引數的意義:

1.URI uri : Image URI;可用的幾種URI:

"http://site.com/image.png" // from Web
"file:///mnt/sdcard/image.png" // from SD card
"file:///mnt/sdcard/video.mp4" // from SD card (video thumbnail)
"content://media/external/images/media/13" // from content provider
"content://media/external/video/media/13" // from content provider (video thumbnail)
"assets://image.png" // from assets
"drawable://" + R.drawable.img // from drawables (non-9patch images)
2.
ImageAware imageAware : 顯示影象的檢視,是androidimageview的包裝類,引數來源是new ImageViewAware(imageView).不能為空
3.DisplayImageOptions options :用於影象解碼和顯示,如果該引數為空則使用預設的option。
4.ImageSize targetSize:影象目標大小。 如果為空 - 大小將取決於檢視。
5.ImageLoadingListener listener:用於影象載入過程監聽。 如果在UI執行緒上呼叫此方法,則監聽器在UI執行緒上觸發事件
6.ImageLoadingProgressListener progressListener:影象載入進度監聽。 如果在UI執行緒上呼叫此方法,則監聽器在UI執行緒上觸發事件。 應在{option選項}中啟用快取磁碟以使此偵聽器正常工作。
這個方法比較長,它的邏輯比較清晰,主要做了下面的方法:
1.判斷各個引數是否合法,是否需要預設值
判斷配置引數和顯示圖片的控制元件是否為空,如果為空直接丟擲了異常
判斷listener options targetsize是否為null,如果為空則使用預設值
判斷uri是否為空,如果uri為空,則在
ImageLoaderEngine中取消該檢視的顯示任務,如果在options中設定了showImageForEmptyUri(R.drawable.ic_empty)則為該檢視顯示一個預設的空uri時的圖片,直接返回。
2.開始監聽下載任務。先從快取中讀取圖片的bitmap,如果快取中有則直接使用,否則需要從磁碟或者從網路下載圖片。
下面就來看如何從快取中讀取,如何下載。
當從memoryCache讀取的bitmap不為null 並且沒有被回收時,就直接展示快取中的這個bitmap。預設情況下options.shouldPostProcess()是false。除非在初始化options選項時設定了postProcesser。
所以我們之間看49行。點開display方法,咦,它是一個介面。
它有幾個實現類分別實現不同的圖片顯示方法。如果在初始化
options選項沒有設定displayer()選項則預設使用SimpleBitmapDisplayer()正常顯示一張圖片。如果設定瞭如下
.displayer(new CircleBitmapDisplayer(Color.WHITE,5))則顯示圓角圖片。

下面以SimpleBitmapDisplayer為例,分析如何實現display的。

1 public final class SimpleBitmapDisplayer implements BitmapDisplayer {
2     @Override
3     public void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom) {
4         imageAware.setImageBitmap(bitmap);
5     }
6 }

這個實現類比較簡單,只有一個方法一句話,可以把它理解成我們在android中給imageView設定圖片的一種方式:imageview.setImageBitmap(bitmap);

ImageViewAware是Android imageView的包裝類,保持ImageView的弱引用以防止記憶體洩漏。如何使用imageview的弱引用這一步暫時忽略,先回到第34行。

以上是快取中有bitmap,下面分析如果從快取中獲取的bitmap為空,則需要載入。因為Androidx.x之後不容許在UI執行緒中做網路載入的操作,所以我們只分析非同步載入的方式,就是第66行engine.submit(displayTask);

在這裡涉及到了一個非常重要的類:ImageLoaderEngine,它負責{LoadAndDisplayImageTask顯示任務}的執行。下面重點解析一下ImageLoaderEngine這個類。
engine這個物件是什麼時候初始化的呢?請回到Imageloader物件的初始化方法init中,第15行: = new ImageLoaderEngine(configuration);
看一下ImageLoaderEngine的構造方法:
1 ImageLoaderEngine(ImageLoaderConfiguration configuration) {
2         this.configuration = configuration;
3 
4         taskExecutor = configuration.taskExecutor;
5         taskExecutorForCachedImages = configuration.taskExecutorForCachedImages;
6 
7         taskDistributor = DefaultConfigurationFactory.createTaskDistributor();
8     }

以taskExecutorForCachedImages 為例,taskExecutorForCachedImages 是一個執行緒池,非同步顯示memory cache裡面的bitmap。

進入submit方法:

 1     /** Submits task to execution pool */
 2     void submit(final LoadAndDisplayImageTask task) {
 3         taskDistributor.execute(new Runnable() {
 4             @Override
 5             public void run() {
 6                 File image = configuration.diskCache.get(task.getLoadingUri());
 7                 boolean isImageCachedOnDisk = image != null && image.exists();
 8                 initExecutorsIfNeed();
 9                 if (isImageCachedOnDisk) {
10                     taskExecutorForCachedImages.execute(task);
11                 } else {
12                     taskExecutor.execute(task);
13                 }
14             }
15          });
16     }

當執行到第10行時,就會呼叫LoadAndDisplayImageTask的run方法,接下來看這個類。這個類的物件封裝了engine和Imageloadinginfo,所以包含了所有的configuration 和options選項。

 

來看它的run方法

 1 @Override
 2     public void run() {
 3         if (waitIfPaused()) return;
 4         if (delayIfNeed()) return;
 5 
 6         ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
 7         L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
 8         if (loadFromUriLock.isLocked()) {
 9             L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
10         }
11 
12         loadFromUriLock.lock();
13         Bitmap bmp;
14         try {
15             checkTaskNotActual();
16 
17             bmp = configuration.memoryCache.get(memoryCacheKey);
18             if (bmp == null || bmp.isRecycled()) {
19                 bmp = tryLoadBitmap();
20                 if (bmp == null) return; // listener callback already was fired
21 
22                 checkTaskNotActual();
23                 checkTaskInterrupted();
24 
25                 if (options.shouldPreProcess()) {
26                     L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
27                     bmp = options.getPreProcessor().process(bmp);
28                     if (bmp == null) {
29                         L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
30                     }
31                 }
32 
33                 if (bmp != null && options.isCacheInMemory()) {
34                     L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey);
35                     configuration.memoryCache.put(memoryCacheKey, bmp);
36                 }
37             } else {
38                 loadedFrom = LoadedFrom.MEMORY_CACHE;
39                 L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey);
40             }
41 
42             if (bmp != null && options.shouldPostProcess()) {
43                 L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey);
44                 bmp = options.getPostProcessor().process(bmp);
45                 if (bmp == null) {
46                     L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey);
47                 }
48             }
49             checkTaskNotActual();
50             checkTaskInterrupted();
51         } catch (TaskCancelledException e) {
52             fireCancelEvent();
53             return;
54         } finally {
55             loadFromUriLock.unlock();
56         }
57 
58         DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
59         runTask(displayBitmapTask, syncLoading, handler, engine);
60     }

 在這裡使用了可重入鎖機制來保證併發操作時資料的完整性。先從快取中獲取到的bitmap==null,看19行,來看這個方法。

 1 private Bitmap tryLoadBitmap() throws TaskCancelledException {
 2         Bitmap bitmap = null;
 3         try {
 4             File imageFile = configuration.diskCache.get(uri);
 5             if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
 6                 L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
 7                 loadedFrom = LoadedFrom.DISC_CACHE;
 8 
 9                 checkTaskNotActual();
10                 bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
11             }
12             if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
13                 L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);
14                 loadedFrom = LoadedFrom.NETWORK;
15 
16                 String imageUriForDecoding = uri;
17                 if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
18                     imageFile = configuration.diskCache.get(uri);
19                     if (imageFile != null) {
20                         imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
21                     }
22                 }
23 
24                 checkTaskNotActual();
25                 bitmap = decodeImage(imageUriForDecoding);
26 
27                 if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
28                     fireFailEvent(FailType.DECODING_ERROR, null);
29                 }
30             }
31         } 
....45 return bitmap; 46 }

 

在這個方法返回了一個解碼後的bitmap,是從磁碟讀取檔案或者網路中獲取的。獲取到bitmap後就回到run方法的58行。這裡將bmp, imageLoadingInfo, engine等封裝了一個runnable的實現類DisplayBitmapTask中。

同樣來看一下這個runnable的run方法。

 1 @Override
 2     public void run() {
 3         if (imageAware.isCollected()) {
 4             L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
 5             listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
 6         } else if (isViewWasReused()) {
 7             L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
 8             listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
 9         } else {
10             L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey);
11             displayer.display(bitmap, imageAware, loadedFrom);
12             engine.cancelDisplayTaskFor(imageAware);
13             listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
14         }
15     }

第11行就回到了display方法,去給我們的ImageView設定bitmap。到此就完成了圖片在控制元件中的展示過程。取消這次任務,並回撥監聽器的onLoadingComplete方法。

接下來我們來分析這個框架是如何載入圖片的bitmap的。回到

tryLoadBitmap()方法的第10行或者25行,看decodeImage(imageUriForDecoding);這個方法
1 private Bitmap decodeImage(String imageUri) throws IOException {
2         ViewScaleType viewScaleType = imageAware.getScaleType();
3         ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
4                 getDownloader(), options);
5         return decoder.decode(decodingInfo);
6     }

圖片的uri等資訊被封裝到了ImageDecodingInfo物件當中。其中getDownloader()返回了ImageDownloader這個介面實現類物件的引用。 那這個ImageDownloader的物件是從哪裡來的呢?我們看LoadAndDisplayImageTask的構造方式中,

downloader = configuration.downloader;
networkDeniedDownloader = configuration.networkDeniedDownloader;
slowNetworkDownloader = configuration.slowNetworkDownloader;

而configuration 是最初在Application類中初始化  ImageLoader 的時候使用者傳遞過來的引數。初始化ImageLoaderConfiguration.Builder時為其設定imageDownloader,如果不設定該變數,則在build()的時候在initEmptyFieldsWithDefaultValues方法中為其初始化一個預設值:

1 if (downloader == null) {
2              downloader = DefaultConfigurationFactory.createImageDownloader(context);
3  }

其實也就是ImageDownloader的一個實現類的物件BaseImageDownloader物件

1 public static ImageDownloader createImageDownloader(Context context) {
2    return new BaseImageDownloader(context);
3  }

 而ImageDownloader對外提供了getStream方法,根據uri獲取輸入流資訊。

1 InputStream getStream(String imageUri, Object extra)

 

然後我們來看decodeImage方法中的decode做了什麼。ImageDecoder是一個介面,裡面只有一個decode方法,它的主要作用就是將獲取到的InputStream轉換成Bitmap。看它的實現類BaseImageDecoder

 1 public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
 2         Bitmap decodedBitmap;
 3         ImageFileInfo imageInfo;
 4 
 5         InputStream imageStream = getImageStream(decodingInfo);
 6         if (imageStream == null) {
 7             L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey());
 8             return null;
 9         }
10         try {
11             imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
12             imageStream = resetStream(imageStream, decodingInfo);
13             Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
14             decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
15         } finally {
16             IoUtils.closeSilently(imageStream);
17         }
18 
19         if (decodedBitmap == null) {
20             L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
21         } else {
//將Bitmap縮放和旋轉成滿足需求的Bitmap
22 decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation, 23 imageInfo.exif.flipHorizontal); 24 } 25 return decodedBitmap; 26 }

 

第5行,這個方法呼叫的就是ImageDownloader的 getStream,並根據uri的Scheme資訊判斷這個圖片是在哪裡,從這裡真正去獲取bitmap。

 1 @Override
 2     public InputStream getStream(String imageUri, Object extra) throws IOException {
 3         switch (Scheme.ofUri(imageUri)) {
 4             case HTTP:
 5             case HTTPS:
 6                 return getStreamFromNetwork(imageUri, extra);
 7             case FILE:
 8                 return getStreamFromFile(imageUri, extra);
 9             case CONTENT:
10                 return getStreamFromContent(imageUri, extra);
11             case ASSETS:
12                 return getStreamFromAssets(imageUri, extra);
13             case DRAWABLE:
14                 return getStreamFromDrawable(imageUri, extra);
15             case UNKNOWN:
16             default:
17                 return getStreamFromOtherSource(imageUri, extra);
18         }
19     }

 

我們以網路獲取為例進行分析:

 1 protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
 2         HttpURLConnection conn = createConnection(imageUri, extra);
 3 
 4         int redirectCount = 0;
 5         while (conn.getResponseCode() / 100 == 3 && redirectCount < MAX_REDIRECT_COUNT) {
 6             conn = createConnection(conn.getHeaderField("Location"), extra);
 7             redirectCount++;
 8         }
 9 
10         InputStream imageStream;
11         try {
12             imageStream = conn.getInputStream();
13         } catch (IOException e) {
14             // Read all data to allow reuse connection (http://bit.ly/1ad35PY)
15             IoUtils.readAndCloseStream(conn.getErrorStream());
16             throw e;
17         }
18         if (!shouldBeProcessed(conn)) {
19             IoUtils.closeSilently(imageStream);
20             throw new IOException("Image request failed with response code " + conn.getResponseCode());
21         }
22 
23         return new ContentLengthInputStream(new BufferedInputStream(imageStream, BUFFER_SIZE), conn.getContentLength());
24     }

 

通過HttpURLConnection從伺服器獲取一個InputStream,封裝在了一個自定義的ContentLengthInputStream中。獲取到InputStream後返回到decode方法的11行.

 1 protected ImageFileInfo defineImageSizeAndRotation(InputStream imageStream, ImageDecodingInfo decodingInfo)
 2             throws IOException {
 3         Options options = new Options();
 4         options.inJustDecodeBounds = true; //true那麼將不返回實際的bitmap物件,不給其分配記憶體空間但是可以得到一些解碼邊界資訊即圖片大小等資訊  
 5         BitmapFactory.decodeStream(imageStream, null, options);
 6 
 7         ExifInfo exif;
 8         String imageUri = decodingInfo.getImageUri();
 9         if (decodingInfo.shouldConsiderExifParams() && canDefineExifParams(imageUri, options.outMimeType)) {
10             exif = defineExifOrientation(imageUri);
11         } else {
12             exif = new ExifInfo();
13         }
14         return new ImageFileInfo(new ImageSize(options.outWidth, options.outHeight, exif.rotation), exif);
15     }

在這個方法中第一次呼叫 BitmapFactory.decodeStream(imageStream, null, options); 獲取圖片的大小解碼邊界等資訊

decode方法的第12行:是重新獲取bitmap,這個我也沒有搞清楚它為什麼又獲取一遍,可能是因為呼叫過來一次decodeStream,但是第一次呼叫只是為了得到圖片的一些資訊,並未得到bitmap,所以需要重新對一個新的inputStream進行decode,
只是這個時候已經知道了圖片的大小 格式等資訊了。就可以直接返回bitmap。

至此,過去bitmap的過程已經完畢,這就是display時呼叫的imageAware.setImageBitmap(bitmap);中的bitmap的由來。

 

相關文章