载入过的图片在滑动出屏幕再滑动回来后的谜之bug,图片变为默认的了。附原理和解决方案
在listview/gridview中使用UIL来display每个item的图片,当图片数量较多需要滑动滚动时会出现卡顿,而且加载过的图片再次上翻后依然会重复加载(显示设置好的加载中图片)
最近在使用UIL遇到了这个问题,相信这个问题许多使用UIL的人都碰到过
现在把解决方法贴出来给有同样问题的朋友做参考
先看下UIL的工作流程
在已经允许内存,存储卡缓存的前提下,当一个图片被请求display时,首先要判断图片是否缓存在内存中,如果false则尝试从存储卡读取,如果依然不存在最后才从网络地址下载
从内存读取的速度最快,存储卡次之,在我们滚动listview的时候,如果是从内存加载图片则会显得非常流畅,如果是存储卡就会先出现载入中图片然后再显示实际图片
我们通常认为已经读过一次的图片自然将会加入内存缓存中,那么下一次读取将是直接从内存中读取,但是实际上载入过的图片在滑动出屏幕再滑动回来后依然会再次从存储卡读取,这主要是UIL的缓存策略引起的一个”疑似BUG”
查看UIL的源码,displayImage函数
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> displayImage(String uri, ImageView imageView, DisplayImageOptions options) {</p><p> displayImage(uri, </span><span style="color: #0000ff;">new</span> ImageViewAware(imageView), options, <span style="color: #0000ff;">null</span>, <span style="color: #0000ff;">null</span><span style="color: #000000;">);</p><p> }</span>
会有ImageAware接口的一个实例化,这个默认的实例化有个重要的参数 :checkActualViewSize 具体说明如下
<span style="color: #0000ff;">public</span><span style="color: #000000;"> ViewAware(View view) {</p><p> </span><span style="color: #0000ff;">this</span>(view, <span style="color: #0000ff;">true</span><span style="color: #000000;">);</p><p> }</p><p> </span><span style="color: #008000;">/**</span><span style="color: #008000;"></p><p> * Constructor</p><p> *</p><p> * </span><span style="color: #808080;">@param</span><span style="color: #008000;"> view {</span><span style="color: #808080;">@link</span><span style="color: #008000;"> android.view.View View} to work with</p><p> * </span><span style="color: #808080;">@param</span><span style="color: #008000;"> checkActualViewSize <b>true</b> - then {</span><span style="color: #808080;">@link</span><span style="color: #008000;"> #getWidth()} and {</span><span style="color: #808080;">@link</span><span style="color: #008000;"> #getHeight()} will check actual</p><p> * size of View. It can cause known issues like</p><p> * <a href="</span><span style="color: #008000; text-decoration: underline;">https://github.com/nostra13/Android-Universal-Image-Loader/issues/376</span><span style="color: #008000;">">this</a>.</p><p> * But it helps to save memory because memory cache keeps bitmaps of actual (less in</p><p> * general) size.</p><p> * <p/></p><p> * <b>false</b> - then {</span><span style="color: #808080;">@link</span><span style="color: #008000;"> #getWidth()} and {</span><span style="color: #808080;">@link</span><span style="color: #008000;"> #getHeight()} will <b>NOT</b></p><p> * consider actual size of View, just layout parameters. <br /> If you set 'false'</p><p> * it's recommended 'android:layout_width' and 'android:layout_height' (or</p><p> * 'android:maxWidth' and 'android:maxHeight') are set with concrete values. It helps to</p><p> * save memory.</p><p> </span><span style="color: #008000;">*/</span></p><p> <span style="color: #0000ff;">public</span> ViewAware(View view, <span style="color: #0000ff;">boolean</span><span style="color: #000000;"> checkActualViewSize) {</p><p> </span><span style="color: #0000ff;">if</span> (view == <span style="color: #0000ff;">null</span>) <span style="color: #0000ff;">throw</span> <span style="color: #0000ff;">new</span> IllegalArgumentException("view must not be null"<span style="color: #000000;">);</p><p> </span><span style="color: #0000ff;">this</span>.viewRef = <span style="color: #0000ff;">new</span> WeakReference<View><span style="color: #000000;">(view);</p><p> </span><span style="color: #0000ff;">this</span>.checkActualViewSize =<span style="color: #000000;"> checkActualViewSize;</p><p> }</span>
这个参数会影响缓存时的key名称,当图片第一次缓存时,当时图片并未下载,自然无法获得图片的长宽尺寸,这时UIL会使用配置预设的maxwidth和maxheight为长宽,缓存的key名称为类似这样:url_widthxheight
在checkActualViewSize设置为true时,第二次载入图片的view将会读取view的长宽,这时的长宽会是图片的实际尺寸,相应的生成的缓存key名称也会变成url_realwidthxrealheight,这个名称同之前缓存的不同,因此也当然不能在缓存查询中命中
所以最后就需要再次从存储中加载图片,并以新的keyname再存一份副本到内存中
解决方法有两个:
1)设置imageview的layout_width和layout_height为实际图片长宽(假如你的图片都是固定尺寸的,这样做就OK了)
2)display的方法修改一下,不直接display imageview改为ImageAware,类似
ImageAware imageAware = <span style="color: #0000ff;">new</span> ImageViewAware(imageView, <span style="color: #0000ff;">false</span><span style="color: #000000;">);</p><p>imageLoader.displayImage(imageUri, imageAware);</span>
显式的将checkActualViewSize设为false, 这样图片的缓存也将只会保存一个副本,保证第二次查询时可以直接命中
按上述方法设置之后一般来说在listview/gridview滑动时图片效果基本没什么大问题了,但是还有些额外设置也许也是大家需要注意的
首先是config的初始化
File cacheDir = StorageUtils.getOwnCacheDirectory(getApplicationContext(), "imageloader/Cache"); <span style="color: #008000;">//</span><span style="color: #008000;">缓存文件的存放地址</span></p><p>ImageLoaderConfiguration config = <span style="color: #0000ff;">new</span><span style="color: #000000;"> ImageLoaderConfiguration </p><p>.Builder(getApplicationContext()) </p><p>.memoryCacheExtraOptions(</span>480, 800) <span style="color: #008000;">//</span><span style="color: #008000;"> max width, max height </span></p><p>.threadPoolSize(3)<span style="color: #008000;">//</span><span style="color: #008000;">线程池内加载的数量 </span></p><p>.threadPriority(Thread.NORM_PRIORITY - 2) <span style="color: #008000;">//</span><span style="color: #008000;">降低线程的优先级保证主UI线程不受太大影响</span></p><p><span style="color: #000000;">.denyCacheImageMultipleSizesInMemory() </p><p>.memoryCache(</span><span style="color: #0000ff;">new</span> LruMemoryCache(5 * 1024 * 1024)) <span style="color: #008000;">//</span><span style="color: #008000;">建议内存设在5-10M,可以有比较好的表现</span></p><p>.memoryCacheSize(5 * 1024 * 1024<span style="color: #000000;">) </p><p>.discCacheSize(</span>50 * 1024 * 1024<span style="color: #000000;">) </p><p>.discCacheFileNameGenerator(</span><span style="color: #0000ff;">new</span><span style="color: #000000;"> Md5FileNameGenerator()) </p><p>.tasksProcessingOrder(QueueProcessingType.LIFO) </p><p>.discCacheFileCount(</span>100) <span style="color: #008000;">//</span><span style="color: #008000;">缓存的文件数量 </span></p><p>.discCache(<span style="color: #0000ff;">new</span><span style="color: #000000;"> UnlimitedDiscCache(cacheDir)) </p><p>.defaultDisplayImageOptions(DisplayImageOptions.createSimple()) </p><p>.imageDownloader(</span><span style="color: #0000ff;">new</span> BaseImageDownloader(getApplicationContext(), 5 * 1000, 30 * 1000)) <span style="color: #008000;">//</span><span style="color: #008000;"> connectTimeout (5 s), readTimeout (30 s)</span></p><p>.writeDebugLogs() <span style="color: #008000;">//</span><span style="color: #008000;"> Remove for release app </span></p><p>.build();
然后是option的设置
options = <span style="color: #0000ff;">new</span><span style="color: #000000;"> DisplayImageOptions.Builder()</p><p>.showStubImage(R.drawable.default_cover)</p><p>.showImageForEmptyUri(R.drawable.default_cover)</p><p>.showImageOnFail(R.drawable.default_cover)</p><p>.cacheInMemory(</span><span style="color: #0000ff;">true</span><span style="color: #000000;">)</p><p>.cacheOnDisc(</span><span style="color: #0000ff;">true</span><span style="color: #000000;">)</p><p>.imageScaleType(ImageScaleType.NONE)</p><p>.bitmapConfig(Bitmap.Config.RGB_565)</span><span style="color: #008000;">//</span><span style="color: #008000;">设置为RGB565比起默认的ARGB_8888要节省大量的内存</span></p><p>.delayBeforeLoading(100)<span style="color: #008000;">//</span><span style="color: #008000;">载入图片前稍做延时可以提高整体滑动的流畅度</span></p><p>.build();
滑动时禁止加载也可以有效的提高表现
setOnScrollListener(<span style="color: #0000ff;">new</span> PauseOnScrollListener(imageLoader, <span style="color: #0000ff;">true</span>, <span style="color: #0000ff;">true</span>));<span style="color: #008000;">//</span><span style="color: #008000;">两个分别表示拖动下拉条和滑动过程中暂停加载</span>
最后就是在getview中,例行的viewholder保存状态之外,将URL存入imageview的tag中,通过对比URL值来减少UIL的display次数以提高表现
UIL是个非常不错的图片加载类的第三方库,可以帮我们在开发过程中省不少事,不过如果想用好也需要自己真正的去研究下
参考资料:
https://github.com/nostra13/Android-Universal-Image-Loader/issues/376
https://github.com/nostra13/Android-Universal-Image-Loader/wiki/Task-flow