最近在使用UniversalImageLoader時遇到了一個小問題,多個地方同時經過ImageLoader.getInstance().loadImage(url, new ImageSize(dp72, dp72)...
加載圖像時,有必定機率只有部分地方能正確地加載到圖片,其餘地方是什麼結果呢?從Log看是這個樣子:java
1 03-19 15:41:44.167 1500-1541/xxx D/ImageLoader﹕ Start display image task [xxxxxxx/group1/M00/00/04/wKgKklUKfS-AGTJRAAAV5nnd6hE739.jpg_144x144]
2 03-19 15:41:44.167 1500-1541/xxx D/ImageLoader﹕ Load image from network [xxxxxxx/group1/M00/00/04/wKgKklUKfS-AGTJRAAAV5nnd6hE739.jpg_144x144]
3 03-19 15:41:44.167 1500-1541/xxx D/ImageLoader﹕ Cache image on disk [cxxxxxxx/group1/M00/00/04/wKgKklUKfS-AGTJRAAAV5nnd6hE739.jpg_144x144]
4 03-19 15:41:44.187 1500-1538/xxx D/ImageLoader﹕ Start display image task [xxxxxxx/group1/M00/00/04/wKgKklUKfS-AGTJRAAAV5nnd6hE739.jpg_144x144]
5 03-19 15:41:44.187 1500-1538/xxx D/ImageLoader﹕ Image already is loading. Waiting... [xxxxxxx/group1/M00/00/04/wKgKklUKfS-AGTJRAAAV5nnd6hE739.jpg_144x144]
6 03-19 15:41:44.199 1500-1541/xxx D/ImageLoader﹕ Cache image in memory [xxxxxxx/group1/M00/00/04/wKgKklUKfS-AGTJRAAAV5nnd6hE739.jpg_144x144]
7 03-19 15:41:44.199 1500-1538/xxx D/ImageLoader﹕ ...Get cached bitmap from memory after waiting. [xxxxxxx/group1/M00/00/04/wKgKklUKfS-AGTJRAAAV5nnd6hE739.jpg_144x144]
8 03-19 15:41:44.219 1500-1500/xxx D/ImageLoader﹕ Display image in ImageAware (loaded from NETWORK) [xxxxxxx/group1/M00/00/04/wKgKklUKfS-AGTJRAAAV5nnd6hE739.jpg_144x144]
9 03-19 15:41:44.219 1500-1500/xxx D/ImageLoader﹕ ImageAware is reused for another image. Task is cancelled. [xxxxxxx/group1/M00/00/04/wKgKklUKfS-AGTJRAAAV5nnd6hE739.jpg_144x144]緩存
有了Log,再結合源碼,看下究竟是什麼緣由,從上面的Log能夠看到,兩個地方加載同一張圖片,都發現緩存中沒有,因此都從網絡上加載(經過分析能夠知道,第1,2,3,6,8是第一個加載的地方的Log,4,5,7,9是第二個加載的地方的Log)。網絡
UniversalImageLoader實際加載圖片的類叫LoadAndDisplayImageTask
,這是一個Runnable
,因此咱們從它的run
方法開始看。首先要強調一點,因爲這兩個地方加載的是相同的Url,而且ImageSize相同,因此它們的memoryCacheKey是相同的,接下來就看run
方法,首先是第一部分代碼。app
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();
因爲memoryCacheKey相同,因此這裏得到的是同一個鎖,結果就是第一個線程鎖住這個鎖進行圖片加載,因此打印出了前3行Log。url
接着輪到第二個線程執行,它發現另外一個線程鎖住了loadFromUriLock
,因此它打印出了第4和第5行Log。線程
而後又換第一個線程執行。code
try { checkTaskNotActual(); bmp = configuration.memoryCache.get(memoryCacheKey); if (bmp == null || bmp.isRecycled()) { bmp = tryLoadBitmap(); if (bmp == null) { return; } checkTaskNotActual(); checkTaskInterrupted(); if (bmp != null && options.isCacheInMemory()) { L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey); // 1 configuration.memoryCache.put(memoryCacheKey, bmp); } } else { loadedFrom = LoadedFrom.MEMORY_CACHE; L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey); // 2 } checkTaskNotActual(); checkTaskInterrupted(); } catch (TaskCancelledException e) { fireCancelEvent(); return; } finally { // 釋放鎖 loadFromUriLock.unlock(); } // 顯示圖片 DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom); runTask(displayBitmapTask, syncLoading, handler, engine);
第一個線程加載完圖片後,在finally
中釋放了鎖,而後經過DisplayBitmapTask
進行圖片的顯示,從Log中能夠分析出,線程中加載完圖片後打印了註釋1處的Log,而後釋放了鎖,輪到線程2執行。圖片
因爲線程一已經加載完圖片並存入了緩存了,因此線程二會進入代碼註釋2的代碼塊,打印出第7行Log。rem
而後線程一線程二再依次運行分別打印第8,9行Log,兩個線程都取到了Bitmap,爲什麼會一個正確加載完圖片,而另外一個有必定機率加載不到呢?這要看UniversalImageLoader的緩存與判斷View重用的機制。get
ImageLoader在加載圖片前會調用ImageLoaderEngine.prepareDisplayTaskFor
方法來記錄一些東西,具體就是記錄一個ImageAware在加載哪個Url,以判斷當圖片加載完成後,這個ImageAware是否被重用來加載其餘的Url了。
private final Map<Integer, String> cacheKeysForImageAwares = Collections.synchronizedMap(new HashMap<Integer, String>()); void prepareDisplayTaskFor(ImageAware imageAware, String memoryCacheKey) { cacheKeysForImageAwares.put(imageAware.getId(), memoryCacheKey); }
能夠看到就是經過一個Map記錄的,以ImageAware的id爲鍵,對於LoadImage方法,使用的是NonViewAware
,它的id是Url.hasCode
,因此兩個地方加載同一個圖片,在cacheKeysForImageAwares
中只有一條記錄。
當加載完圖片後是經過DisplayBitmapTask
來顯示圖片並回調咱們的Listener的。
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); } }
void cancelDisplayTaskFor(ImageAware imageAware) { cacheKeysForImageAwares.remove(imageAware.getId()); }
private boolean isViewWasReused() { String currentCacheKey = engine.getLoadingUriForView(imageAware); return !memoryCacheKey.equals(currentCacheKey); }
當第一個地方執行到這個run方法時,會走到else分支裏,打印出第8行的Log,而後調用ImageLoaderEngine.cancelDisplayTaskFor
方法,移除在Map中的記錄,並回調咱們的Listener。
而後第二個地方執行到run中的isViewWasReused
方法時,因爲Map中的記錄已經被第一個線程移除了,因此取得的currentCacheKey是null,就會斷定爲View被重用了,因此不能獲得正確的結果。
那麼爲何有時兩個地方能同時獲得正確的結果呢?那是由於若是當第一個線程進入到else代碼塊但在執行cancelDisplayTaskFor
以前進行了線程調度,另外一個線程仍是有機會同時進入else代碼塊的。
其實對於NonViewAware,基本是不可能被重用的,因此感受在這裏能夠作下特殊處理,或者對其生成 id的方法進行下修改(但這樣會屢次從網絡取同一張圖片)。或者像Volley同樣,當執行一個請求時,若是發現這個圖片正在Loading,就將其加入一個列表,當加載完後統一貫這個列表裏的請求發送消息,但這個修改就比較麻煩了,因此仍是對NonViewAware作下特殊處理比較好,畢竟這個基本是不可能被重用的。