最近公司有一个项目需要支持手机本地图片的多选,就像微信那样的。
OK,不能调用系统的图片选择控件,那就自己写个吧,基本思路就是使用ContentProvider扫描手机中的图片,然后以Gridview的方式展示图片,同时为了保证能图片能快速加载,需要对图片进行缓存(内存缓存是必须的,由于本来就是本地的图片,暂时可以不用再在SD卡中缓存)。
ContentProvider扫描手机中的图片好办,关键是如何更快的加载图片。
最初想到的就是使用开源图片加载框架 UIL , 作为目前使用最广泛的图片加载框架,不用真是可惜了,于是优先考虑它了。
Demo很快就写好了,大部分手机上也测试OK,但是在一台小米3手机上出现了内存溢出的问题,如下图(OOM导致了部分图片出现加载失败):
究其原因,米三手机的分辨率(1920*1080)太大,拍照拍出来的图片也大(一般都在2M左右),当一次加载的图片太多的时候就容易出现内存溢出的情况。为了不占用太多的内存,我按照官方的说法(见UIL项目介绍)做了如下调整:
1. 将内存缓存的图片尺寸改小(之前没有指定,UIL框架默认会按照手机的分辨率来确定缓存图片的尺寸):
ImageLoaderConfiguration.Builder builder = new ImageLoaderConfiguration.Builder(context)
...
.memoryCacheExtraOptions(480, 800) // default = device screen dimensions
.discCacheExtraOptions(480, 800, CompressFormat.JPEG, 75, null)
...
2. 将显示时的图片的质量降低、同时图片缩放:
DisplayImageOptions options = new DisplayImageOptions.Builder()
...
.bitmapConfig(Bitmap.Config.RGB_565)
.imageScaleType(ImageScaleType.EXACTLY)
...
.build();
OK,内存溢出的现象是不见了,但是新的问题出现了:.imageScaleType
但是却不能根据需要来缩放,所以种种原因迫使我放弃使用UIL来加载本地图片。
在决定放弃使用UIL加载本地图片之后,我开始自己写缓存图片的方案,我的想法是只在内存中缓存就好,没必要再在本地磁盘中缓存一份,同时为了保存不OOM,需要限定缓存图片的规格。
于是我专门写了一个加载本地图片的类,使用android支持包中的LruCache作为内存缓存,根据要显示图片的ImageView控件的宽和高来缩放图片以降低内存使用量。使用时也比较简单:
由于使用LruCache,出现内存不足的时候,系统会自动gc,所以一般情况不会出现OOM的情况。同时由于没有缓存的时候都会新建一个线程用于加载图片所以加载图片的速度也还可以接受。
但是问题是,当图片多的时候,频繁新建线程的内存开销会比较大,这样会导致UI线程卡慢的情况(ListView或GridView滚动不流畅)。出现这种情况后我立马就想可以用线程池来代替每次都新建线程的情况。
用线程池代替Thread比较简单,Java中的Executors类以工厂模式的方式提供了一些快速创建常用的线程池的方法。例如:
Executors.newFixedThreadPool(int nThreads);
Executors.newSingleThreadExecutor();
Executors.newCachedThreadPool();
Executors.newScheduledThreadPool(int nThreads);
使用线程池后UI主线程不卡了,但又有问题,当图片多的时候,Gridview滑动到底部,这时候图片可能要等很久才能加载出来。这种情况很好理解,当图片很多的时候,线程池中的那几个线程根本不够用,所以这时候图片的加载还是会表现出一定的顺序性。如果直接滑动到GridView或ListView的底部,图片自然要等一段时间才能加载出来。
于是乎我想,可以通过监听ListView/GridView的滚动来控制图片是否加载,当控件滚动的时候不加载图片,只有当其静止的时候才加载。
第二个问题是目前最棘手的问题。如果优先加载ListView/GridView可见区域的图片而暂时忽略不可见部分的图片,由于可见部分的图片数量比较少,即使单个图片比较大也能在短时间内加载完成。这样不管用户想要看前面的图片还是后面的图片都能在短时间内在界面上显示,这样用户的体验会比较好。
前面讲到的监听控件的滚动事件其实际也是优先加载可见区域的图片。那么可不可以用其他方法优先加载可视化区域的图片呢?
经过一段时间的探索,我的最终方案确定了: 由于在ListView/GridView滚动的时候会调用其adapter的getView方法(该方法中发送加载图的请求),而且是只有当ListView或GridView中的Item可见的时候才会调用getView方法, 这样我们就可以人为定义图片加载的优先级了,总结起来一句话:“后来居上”。 具体实施方案如下:
这样最终效果还不错,也达到了我与其的效果。