加载图片的一些琐碎优化记录

加载图片的一些琐碎记录

作者:∞_小低调的过日发布于 3小时前访问(7)评论(0

有关图片加载的一些记录。这里针对的主要是会有大量的AdapterView需要快速滚动加载图片情况

一些如 异步加载,文件缓存,LruCache内存缓存Bitmap等的常规的通用方式就不在这里说,这些可以看谷歌给的例子

这里单说一些使使用了前边所说的方式,依然有时候加载不流畅的情况

1、线程优先级
可能有时候发现使用了线程池异步加载,但是在图片加载密度很大的时候,在部分性能不好的机子上,界面还是有点卡,那有可能的原因是子线程优先级太高了

因为正常创建的子线程的优先级都是 Thread.NORM_PRIORITY。当机子性能不好,cpu竞争,有时候会导致主线程卡顿

解决方案:降低优先级

线程池在构建的时候使用

new ThreadPoolExecutor(REMOTE_NUMBERS, REMOTE_NUMBERS*2, 60,
TimeUnit.SECONDS, new LinkedBlockingDeque(),
new InefficiencyThreadFactory());

其中,线程构建工厂使用

复制代码
private static class InefficiencyThreadFactory implements ThreadFactory {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "Imageloader #" + mCount.getAndIncrement());
thread.setPriority(Thread.MIN_PRIORITY+1);
return thread;
}
}
复制代码

其实,不止图片加载的线程需要这样操作。如果做后台不间断一直在处理数据的的线程,比如说遍历生成手机内所有应用的MD5码,如果线程为普通优先级,性能比较弱的设备上也可能会导致界面卡顿

2、File对象构建和检索
第一步的优化,发现界面流畅好多,但是本地缓存越来越多的时候,发现快速滑动的时候,还会出现卡顿情况,经测试是因为主线程中调用new File()和调用File.exists导致的

因为我在项目里边最开始的设定是,先判断本地是否有缓存,然后决定从本地decode还是从服务器加载,虽然后续步骤都是在子线程中操作的,但是之前的判断对文件检索的时候还是会耗时,导致卡顿。

解决方案: 把判断方法放入子线程中 。ps:看了一些开源的加载工具类,有的会做文件缓存队列。

遇到的一些其他问题
1、防重复提交导致的第一张图片加载不出来的问题
ps:如果说,你的项目中没有出现这个问题,可能有两个原因,一是你没有做防重复请求机制 ,二可能是你已经有更完善的解决方案了

问题产生的原因:

小问题1: ListView或者别的布局中很可能会出现两张相同图片同时显示的情况,也就可能会导致会在同一时间对同一个图片发起请求。但是又不应该对同一张图片同时做网络请求或者做读取磁盘的操作,这就涉及到防止重复提交的问题了。这样的小问题可以在,第二次请求前拦截,解决代码很简单,不算问题。

小问题2: 正常来说,AdapterView(Gallery,ListView) 加载图片大多人会放在getView的时候进行异步加载。但是,常用的猿媛们应该会发现,AdapterView显示在第一个位置组件在创建的时候或者nofifyDataChaged的时候,会多次(一般两次,受getViewTypeCount影响)调用getView。因为第一次调用的getView获得的组件只是用来父组件查看子组件的一些的布局参数,而不用来显示,第二次调用getView获取的组件才会放入父容器中用来显示。只是android这样的机制其实对程序员正常写代码的时候没有影响的,也不算问题。

恩恩,对的,你应该猜到了,两个都算是小问题的问题,碰到一个就出现了一个蛋疼的问题: 当小问题2中的情况下,第一个位置 第一次调用getView获得ImageView1,这时候ImageView1开始加载图片,加载的同时,第二次getView调用,并获得ImageView2,也开始加载图片。这两个ImageView是同一个位置创建的,请求的url肯定也会相同。但是,因为ImageView1正在加载图片,从而导致ImageView2被防重复请求机制拦截请求而加载失败。一段时间后,ImageView1加载图片成功,并设置成功。 结果是,ImageView1加载成功,但是并没有放入父类组件中用来显示。ImageView2才是父类真正用来显示的组件,却加载失败。 而导致第一个显示位置的图片加载不出来。

曾经也针对android这个机制出现的问题写了一个 解决方案 ,这个方案不仅麻烦,而且还只是曲折的解决了AdapterView中部分情况下的问题。如果LinearLayout中有两个或多个ImageView加载同一个图片,可能会导致只有第一张图片出来,其他的都因为防重复提交拦截而加载失败导致没法显示。

现在的解决方案:

下边是runnable中粗略的代码

View Code
ok, 问题解决

2、快速滚动延迟加载方案--减少快速滑动中,不显示图片的加载(这条不想贴代码了,阐述的有点混乱)
如果涉及到快速滚动,也必然是AdapterView的图片加载,而AdapterView中又是在getView中请求图片的。

就会涉及到一个问题,如果快速滚动的时候,很可能从第一个位置在几秒内滚动到第几百的位置上。如果每个位置滑过的时候都进行了图片处理,则会有几百张图片要去加载,如果网络正常的话,你想看到你当前显示的组件的那些图片,少的也得一两分钟吧?用户怎么可能忍受? 所以,必须引进延迟加载。

如果只是简单认为,在OnScrollListener.SCROLL_STATE_FLING的状态下,getView 不调用图片加载方法的话,那可能当快速滑动停下来的时候,当前界面的图片也都加载不出来了。因为显示的在屏幕上的组件也是在SCROLL_STATE_FLING状态下调用getView出来的,也没有调用图片加载方法。 如果非要用这样来做延迟加载的,或许 在onScrollStateChanged 这个方法中,SCROLL_STATE_IDLE状态时,notifyDataChanged一下也许可以,但我没试过。

事实上,大多延迟加载都是基于下边两点来实现的

|- 大多图片加载都要引用线程池或自己维护线程队列来实现.所以,就算getView中调用了图片加载方法,任务也是放在队列中等待执行。

|- AdapterView中的getView获取的组件是复用的,虽然滚动了几个百个item,但实际上初始化的子组件只比显示出来的组件个数多几个。

如果没有维护图片加载队列且AdapterView没有复用组件,这条下边说的内容可以略过了

设定: 要加载的ImageView 为 iv,图片uri 为 uri

方案A:我的方案
在getView 中,调用iv.setTag(uri) ,缓存了要加载图片的路径

当在子线程请求方法执行之前,先比较(时间点在任务执行的时候)要请求的图片路径与 iv.getTag 是否相同,如果不相同,说明iv 已经被再次getView,且有新的图片要显示。则当前请求的uri没有必要继续请求,结束这个任务。如果是,则当前iv没有更新的图片来显示,则加载

只是这样做的话,线程池会按滚动顺序,依次队列中取出请求任务来判断然后执行。实际上,往往用户当前要看的图片的任务是最近才放入队列中的,如果按FIFO顺序,会请求不及时,所以,我做了一个LIFO队列

//LinkedBlockingDeque 这个类1.6才有 即2.3以上

复制代码
private static class LoaderDeque extends LinkedBlockingDeque{
private static final long serialVersionUID = 700662893561342216L;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    @Override 
    public boolean offer(T e) { 
        return super.offerFirst(e); 
    }

    @Override 
    public T remove() { 
        return super.removeFirst(); 
    } 
}

复制代码
使用其实和最开始讲到的一样

1
2
3
new ThreadPoolExecutor(REMOTE_NUMBERS, REMOTE_NUMBERS*2, 60, 
                TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(), 
                new InefficiencyThreadFactory());

注意,设置Bitmap的时候,也一定要再次检查请求到的Bitmap的url和iv.getTag是否相同,相同才设置

这个处理方案,缺点是占用了ImageView 的tag 对象。

1
可优化的方法: 使用一个HashMap ,里边存入ImageView.hashCode 和 图片url 的键值对。从而释放了ImageView的tag引用。 注意,这里使用了ImageView.hashCode ,而没有直接使用ImageView,可以避免不必要的内存泄露。 如果想用ImageView做key,建议使用 WeakHashMap

方案B. 谷歌提供的图片加载示例代码的延迟方案
要加载图片的ImageView中设置了一个Drawable的包装类。这个包装类中用弱引用引用了一个BitmapWorkerTask(AsyncTask的子类)对象实例

当getView调用,并且开始加载图片时,会拿出Drawable 中的引用 BitmapWorkerTask 来比较(时间点是任务提交的时候),是否是当前任务,如果不是当前任务,说明iv需要重新加载,则把原来的task停止,并把由请求Uri生成的BitmapWorkerTask 通过Drawable包装类缓存,并设置到iv上。这样也避免了加载当前不显示的图片。

而且谷歌图片加载实例还预留了一个在快速滑动的时候,暂停加载的接口

关键的两个变量

1
2
3
 boolean mPauseWork    

 Object mPauseWorkLock = new Object();

当AdapterView 为SCROLL_STATE_FLING状态的时候,将mPauseWork 设置为true

而在任务的网络请求或磁盘读取之前,判断是否暂停加载,如果暂停,则使用mPauseWorkLock.wait 阻塞线程。而当滑动状态发生改变为停止的时候,会把mPauseWork 设置为false,并且mPauseWorkLock .notifyAll 唤醒所有阻塞线程。

1
2
3
4
5
6
7
8
9
    synchronized (mPauseWorkLock) { 
           while (mPauseWork && !isCancelled()) { 
               try { 
                   mPauseWorkLock.wait(); 
               } catch (InterruptedException e) {} 
           } 
       }

      //request

感觉篇幅太长了。延迟加载神马的代码就不上了。

3、貌似所有的问题解决了,还是卡--部分机型的文字渲染导致的卡顿
我这里还遇到过一个情况,所有的机型都测试流畅,只有部分机型卡顿。后来测试发现,如果ListView换成纯图片滑动的挺欢的,换成纯文本就会卡顿。代表机型有 谷歌2代,HTC C8812E

测试用例中,ListView 每一个item设置的文字为20个左右,且没有使用重复数据(我截取了大约2000字小说内容,按每20个文字截取一个字符串),滑动会卡顿严重,但是如果每个item中的文字换成相同的或者相同文字重复量大,则不再卡顿

后来测试HTC C8812E 只有第一次加载的时候会卡,滚动结束后,再次进入测试应用就不会再卡了。但是谷歌2代会一直卡。

因为这两款机型的运存都比较低,推测是,字符库缓存策略类似应用的res目录下资源文件的缓存策略。

大量不同文本的话,检查到缓存字符集中没有,就会不断从字符库中加载字符,但是又因为内存偏小,可以缓存的字符集有限,从而导致之前加载的回收,GC,然后再加载..

1
   这类问题只能无视了,或者在这些机型上降低不同文字数量。但是不要担心你的图片加载策略了,因为纯图片加载没问题。

总结

降低线程优先级
File对象构建和检查存在放入子线程中
第一张图片加载不出来解决方案
延迟加载解决方案
界面卡顿也可能是文字渲染导致

声明:eoe文章著作权属于作者,受法律保护,转载时请务必以超链接形式附带如下信息

原文作者: ∞_小低调的过日

原文地址: http://my.eoe.cn/wxjlovezsy/archive/18298.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值