文章出處

最近在使用UniversalImageLoader時遇到了一個小問題,多個地方同時通過ImageLoader.getInstance().loadImage(url, new ImageSize(dp72, dp72)...加載圖像時,有一定機率只有部分地方能正確地加載到圖片,其他地方是什么結果呢?從Log看是這個樣子:

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方法,首先是第一部分代碼。

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。

接著輪到第二個線程執行,它發現另一個線程鎖住了loadFromUriLock,所以它打印出了第4和第5行Log。

然后又換第一個線程執行。

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。

然后線程一線程二再依次運行分別打印第8,9行Log,兩個線程都取到了Bitmap,為何會一個正確加載完圖片,而另一個有一定機率加載不到呢?這要看UniversalImageLoader的緩存與判斷View重用的機制。

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做下特殊處理比較好,畢竟這個基本是不可能被重用的。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()