为什么不推荐用UniversalImageLoader加载res/drawable

来源:互联网 发布:超薄笔记本推荐 知乎 编辑:程序博客网 时间:2024/04/29 16:21

前言

UniversalImageLoader是一个应用广泛的库, 使用简单方便, 编译也方便, 很多大公司都是用的这个库, 最近遇到了一个奇怪的bug, 踩中了UIL的一个坑, 虽然作者有在wiki上提醒我们, 但因为语焉不详, 所以直到我找到bug的出现原因才想明白那句话是什么意思.

一个奇怪的bug

项目里面有一个自绘控件, 是一个下载按钮, 点击之后可以显示当前下载进度, 最近我们发现在一台2.3.7和一台4.2.2上这个按钮显示的图片不正确, 其他手机正常, 但debug后发现按钮的onDraw方法调用是没有问题的. 而图片是res/drawable目录下的.png文件, 照理来说这种图片是不可能读错的.
通过回滚commit, 定位到一次merge提交, 是从功能开发分支回主线的merge, 仔细review了提交, 发现这次commit根本没有改动按钮的代码, 按钮使用的图片资源也没有被篡改, 我们本地一个一个的提交更改, 发现只要res/drawable目录下新增一个xml文件, 按钮的图片就会错乱, 在所有的xml文件名上增加wtf_前缀, 那两台手机的上bug就消失了, 但其他所有正常的机器又开始出现bug了.
随后我检查了R.java文件, 里面按钮所使用的图片都具有唯一的id. 反编译安装包, 在smali中出现的id和public.xml里面出现的id一致, 且没有id相同的情况. 我们一度以为是android的bug导致其加载了错误的图片.

UIL的惊天bug

之后在看自绘控件onDraw方法时, 我看到他使用了ImageLoader.loadImageSync方法获取他需要使用的图片, 使用的是类似drawable://[R.drawable.picname]这种ImageLoader自己定义的协议, 用于读取res/drawable目录下的非9patch图片, 我问他为什么要这么做, 他说这样可以利用ImageLoader的缓存机制减少读取图片时的文件IO, 嗯, 没问题.
尽管我相信UIL使用了这么久, 不可能有读取错误图片这么严重的bug存在, 但出于排查bug的目的, 还是对其进行了测试, 测试方法很简单:
四个ImageView, 两个使用原生apisetImageResource分别加载A图和B图, 另外两个使用ImageLoader.loadImageSync方法分别加载A图B图. 注意这俩图都是在res/drawable目录下的.
结果令我吃惊, UIL加载的两张图竟然一模一样, UIL是bug的根源.
那一刻我觉得自己好像发现了UIL一个惊天bug, 恨不得马上给UIL来个pull request. 不过我猜肯定是我们哪里用的不对, 我记得官方wiki上不建议用ImageLoader加载drawable, 建议我们用原生方法, 难道是我们踩到坑了?

追查UIL

回去debug, 发现那俩错乱的drawable在存储器上对应两个文件, 应该是ImageLoader缓存在磁盘上的, 我们给它的默认配置就是在内存和磁盘上都缓存图片, 我打开那俩文件, 发现他们是一张图, 我是奇怪怎么会这样.
仔细看了ImageLoader的源码, 主要看它是如何处理drawable的, 并没有发现会导致它加载错误图片的异常, 只是它会把drawable缓存一份在存储器上, 文件名是用R.drawable.name做MD5算出来的.
每次要获取一张图片, ImageLoader首先在memorycache中检查有没有这张图, 如果没有再到diskcache中检查, 在diskcache中标识这张图的方法就是对其URI进行MD5运算, 如果diskcache中存在这个文件名对应的文件, 那么就将这个文件decode成一个bitmap给app使用.
当时我手机上上已经有被缓存的图片了, 我想看看ImageLoader是怎么把两个id对应的图片读成一个的, 清除数据再debug, 意外的是这次读取的图片是正确的. 我又想起我们要求ImageLoader在磁盘上缓存图片……

我们的锅

ImageLoader帮我们将图片缓存在磁盘上, 用drawable id的MD5值作文件名. 原来是这样.
开发android的时候经常遇到R.java不能生成的情况, 这个R.java里记录了很多资源文件的id, 这都是eclipse帮我们做的, 这样android就能通过这些id找到对应的资源. 生成id的顺序是一致的, 所以当我们向res/drawable中添加一个xml文件, 那么drawable生成的id必然会有变化, 某些drawable对应的id会+1, 假如1.0版本A图的id是0x00000001, B图的id是0x00000002, 我们在2.0版本添加一个C图, 那么有可能之后生成的id为 A图0x00000002, B图0x00000003.
假如我们忽略MD5过程, 直接认为ImageLoader使用id做文件名(原理上一样, 只不过用MD5更好), 使用过1.0版本的手机, diskcache上已经有了0x00000001.png(1.0版本A图)0x00000002.png(1.0版本B图)两个文件, 升级到2.0版本, app用ImageLoader加载A图时, 会用A图的最新id 0x00000002来查找, 此时磁盘上恰好有0x00000002.png(1.0版本B图), 于是ImageLoader就将B图加载到内存了, 于是bug就出现了.

规避方法

推荐: 在使用ImageLoader加载res/drawable时务必将cacheOnDisk设为false.
当然你也可以修改UniversalImageLoader的源码, 毕竟drawable本身可以理解为在磁盘上有永久缓存的资源, 在遇到此类资源时, 如果memorycache中没有, 直接通过系统api请求, 而非读取自己管理的diskcache, 当然这种做法就会使代码显得有些不通用了, 不建议这么改.

1 0
原创粉丝点击