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

UniversalImageLoader的一个小问题的更多相关文章

  1. Redola.Rpc 的一个小目标

    Redola.Rpc 的一个小目标 Redola.Rpc 的一个小目标:20000 tps. Concurrency level: 8 threads Complete requests: 20000 ...

  2. 与大家分享robotium一个小问题。Test run failed:Instrumentation run failed due to 'java.lang.ClassNotFoundException'

    今天和大家分享robotium一个小问题. 我们在运行自已经搭好的框架时,有可能会出现一个找不到类的错误(如上图所示). 问题是重签名工具给出的activity有误,这时我们可以用Appt命令查看重签 ...

  3. 用struts2标签如何从数据库获取数据并在查询页面显示。最近做一个小项目,需要用到struts2标签从数据库查询数据,并且用迭代器iterator标签在查询页面显示,可是一开始,怎么也获取不到数据,想了许久,最后发现,是自己少定义了一个变量,也就是var变量。

    最近做一个小项目,需要用到struts2标签从数据库查询数据,并且用迭代器iterator标签在查询页面显示,可是一开始,怎么也获取不到数据,想了许久,最后发现,是自己少定义了一个变量,也就是var变 ...

  4. 关于SQL Server镜像的一个小误区

    昨天晚上突然接到客户的电话, 说在配置了镜像的生产环境数据库下修改 “已提交读快照” 选项的时候报错, 需要先取消镜像然后再重新搭建.悲催的是这是个近TB的数据库,问我有没有什么快速的方法.于是我就问 ...

  5. 先定一个小目标,自己封装个ajax

    你是否发现项目中有很多页面只用到了框架不到十分之一的内容,还引了压缩后还有70多kb的jquery库 你是否发现项目中就用了两三个underscore提供的方法,其他大部分的你方法你甚至从来没有看过 ...

  6. 快速掌握iOS API的一个小技巧

    快速掌握iOS API的一个小技巧 周银辉 iOS SDK和Developer Library中提供了各个类以及函数的帮助文档,这很棒,但要想了解整个库的大体结构(比如UIKit下有哪些类,他们的继承 ...

  7. Python2 下 Unicode 的一个小bug

    关于Python的编码问题已经是老生常谈了,此处主要是介绍一个罕见的问题,也算是Python2的一个bug了(Python3不会有此问题). 在有时候我们去爬取网页或者调用一些第三方库获取文本的时候, ...

  8. 用android去写一个小程序

    前言: 软工的一个小作业:实现"黄金分割小游戏", 需要结对编程,队友:陈乐云    共用时两天. 早期思路设计: 采用键值对的形式,以Map作为存储结构.优点:能够将数据与用户对 ...

  9. js中关于value的一个小知识点(value既是属性也是变量)

    今天在学习input的value值时,发现这么一个小知识点,以前理解不太透彻. [1]以下这种情况是常见情况,会弹出“测试内容” <input type="button" v ...

随机推荐

  1. Sublime Text3安装JsHint

    介绍 Sublime Text3使用jshint依赖Nodejs,SublimeLinter和Sublimelinter-jshint. NodeJs的安装省略. 安装SublimeLinter Su ...

  2. jQuery实践-网页版2048小游戏

    ▓▓▓▓▓▓ 大致介绍 看了一个实现网页版2048小游戏的视频,觉得能做出自己以前喜欢玩的小游戏很有意思便自己动手试了试,真正的验证了这句话-不要以为你以为的就是你以为的,看视频时觉得看懂了,会写了, ...

  3. Linq表达式、Lambda表达式你更喜欢哪个?

    什么是Linq表达式?什么是Lambda表达式? 如图: 由此可见Linq表达式和Lambda表达式并没有什么可比性. 那与Lambda表达式相关的整条语句称作什么呢?在微软并没有给出官方的命名,在& ...

  4. 【原】Github+Hexo+NextT搭建个人博客

    摘要 GitHub 是一个开源项目的托管网站,相信很多人都听过.在上面有很多高质量的项目代码,我们也可以把自己的项目代码托管到GitHub,与朋友们共享交流.GitHub Pages 是Github为 ...

  5. 8、Struts2 运行流程分析

    1.流程分析: 请求发送给 StrutsPrepareAndExecuteFilter StrutsPrepareAndExecuteFilter 询问 ActionMapper: 该请求是否是一个 ...

  6. Xamarin+Prism小试牛刀:定制跨平台Outlook邮箱应用(后续)

    在[Xamarin+Prism小试牛刀:定制跨平台Outlook邮箱应用]里面提到了Microsoft 身份认证,其实这也是一大块需要注意的地方,特作为后续补充这些知识点.上章是使用了Microsof ...

  7. 新手学习web遇到的一些乱码问题

    在新手学习web网站学习的时候经常会遇到?????这种乱码,对于刚起步的菜鸟来说真的很头痛,很容易打击继续学的信心当然了对于菜鸟的我最近也遇到过乱码问题,沉浸其中不能自拔,爱的深啊!!!!!我所遇到的 ...

  8. 使用github远程仓库

    经过几天对github的研究,终于把自己想完成的给解决了,发现google真的有很多解释,但是很多也会出现一些bug,对于初学者真的很多烦恼,所以整理一份,能给初识github的你有所帮助 一,首先, ...

  9. 安卓自定义组合控件--toolbar

    最近在学习安卓APP的开发,用到了toolbar这个控件, 最开始使用时include layout这种方法,不过感觉封装性不好,就又改成了自定义组合控件的方式. 使用的工具为android stud ...

  10. 嵌入式C语言代码的调试技巧

    在项目开发的过程中,不可避免的会遇到调试代码的情况. 刚开始写代码时,我们想看具体执行到哪儿时,往往这么写: printf("***** Code is here! *****\n" ...