UniversalImageLoader的一个小问题

最近在使用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作下特殊处理比较好,毕竟这个基本是不可能被重用的。

相关文章
相关标签/搜索