加载图片的一些琐碎记录

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

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

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

1、线程优先级

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

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

  解决方案:降低优先级

线程池在构建的时候使用

new ThreadPoolExecutor(REMOTE_NUMBERS, REMOTE_NUMBERS*2, 60, 
                    TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(), 
                    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中粗略的代码

/** 
* 
* @author boliu 
* @param <Request> 
* @param <Result> 
*/ 
public class RequestRunnable<Request,Result> implements Runnable { 
    String url ; 
    Client<Request, Result> client; 
    public RequestRunnable(String url,Client<Request, Result> client) { 
        this.url = url; 
        this.client = client; 
    } 
    @Override 
    public void run() { 
        ReentrantLock lock = null; 
        try { 
            lock = getRequestLock(url); 
            if(lock!=null){ 
                lock.lock();// 如果已经有这个URL的请求正在执行,这个方法会被阻塞.当这个锁被另外个线程释放,这条线程就会被唤起 
            } 
            Result result; 
            result = preRun();// 获取锁之后先从缓存中检查是否已存在缓存(包括磁盘缓存,或者内存中的缓存)。因为可能在前边lock阻塞的时候,已经被其他线程加载完毕,并放入缓存了 
            if(result==null){ 
                if(!Thread.currentThread().isInterrupted()) { 
                    result = client.execute(getRequest(url)); 
                }else{ 
                    throw new InterruptedException(); 
                } 
            } 
            postResult(result); 
        }catch (Exception e) { 
            //请求失败,设置失败图片 
        } finally { 
            if (lock != null&&lock.isHeldByCurrentThread()) { 
                lock.unlock(); 
            } 
        } 
    } 
    private void postResult(Result result) { 
        // 通过handler刷新设置图片 
    }

    private Result preRun() { 
        // 获取缓存 
        return null; 
    } 
    private Request getRequest(String url){ 
        // 通过url生成请求的对象 
        return null; 
    } 
    private static WeakHashMap<String,ReentrantLock> REQUET_LIST = new WeakHashMap<String,ReentrantLock>();

    public static synchronized  ReentrantLock getRequestLock(String url){ 
        if(url!=null){ 
            ReentrantLock lock = REQUET_LIST.get(url); 
            if(lock==null){ 
                lock =  new ReentrantLock(); 
                REQUET_LIST.put(url, lock); 
            } 
            return lock; 
        } 
        return null; 
    } 
}

 

 

/** 
     * 请求过程自定义 
     * @param <Request> 
     * @param <Result> 
     */ 
    public interface Client<Request,Result>{ 
        Result execute(Request request); 
    } 
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<T> extends LinkedBlockingDeque<T>{ 
        private static final long serialVersionUID = 700662893561342216L;

        @Override 
        public boolean offer(T e) { 
            return super.offerFirst(e); 
        }

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

  使用其实和最开始讲到的一样 

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

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

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

    可优化的方法: 使用一个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上。这样也避免了加载当前不显示的图片。

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

关键的两个变量

     boolean mPauseWork    

     Object mPauseWorkLock = new Object();

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

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

        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. 降低线程优先级
  2. File对象构建和检查存在放入子线程中
  3. 第一张图片加载不出来解决方案
  4. 延迟加载解决方案
  5. 界面卡顿也可能是文字渲染导致
原文地址:https://www.cnblogs.com/boliu/p/3372801.html