关于图片加载优化相关方案 -- android,ios

1、快速滑动图片加载性能优化方案

两种方案:
1:加载策略
2:手动控制

1.1、 加载策略

1):FIFO first in first out
2):LIFO last in first out

后进先出,针对滑动加载图片的,这个比较合适,滑动速度越快,越能体现这种方案的优势。当前呈现给用户的,最新加载;当前未呈现的,选择加载。这种方案需要自己写工具类控制线程调度,也就相当于控制多线程并发。如下:

/**
 * Created by zyj on 2017/9/21.
 */

public class TaskDispatcher {
    private LinkedList<Runnable> mTaskList;         // 任务队列
    private ExecutorService mThreadPool;            // 线程池
    private Thread mPollingThead;                   // 轮询线程
    private Handler mPollingHanler;                 // 轮询线程中的Handler
    private static int mThreadCount = 3;            // 线程池的线程数量,默认为3
    private Type mType = Type.FIFO;                 // 队列的调度方式,默认为LIFO
    private volatile Semaphore mPollingSemaphore;   // 信号量,由于线程池内部也有一个阻塞线程,若加入任务的速度过快,LIFO效果不明显
    private volatile Semaphore mSemaphore = new Semaphore(0);  //信号量,防止mPoolThreadHander未初始化完成

    private static TaskDispatcher mInstance;

    public enum Type { FIFO, LIFO }

    /**
     * 单例获得实例对象
     * @return   实例对象
     */
    public static TaskDispatcher getInstance() {
        if (mInstance == null) {
            synchronized (TaskDispatcher.class) {
                if (mInstance == null) {
                    mInstance = new TaskDispatcher(mThreadCount, Type.FIFO);
                }
            }
        }
        return mInstance;
    }

    /**
     * 单例获得实例对象
     * @param threadCount    线程池的线程数量
     * @param type           队列的调度方式
     * @return   实例对象
     */
    public static TaskDispatcher getInstance(int threadCount, Type type) {
        if (mInstance == null) {
            synchronized (TaskDispatcher.class) {
                if (mInstance == null) {
                    mInstance = new TaskDispatcher(threadCount, type);
                }
            }
        }
        return mInstance;
    }

    /**
     * 构造函数
     * @param threadCount    线程池的线程数量
     * @param type           队列的调度方式
     */
    private TaskDispatcher(int threadCount, Type type) {
        init(threadCount, type);
    }

    /**
     * 初始化
     * @param threadCount    线程池的线程数量
     * @param type           队列的调度方式
     */
    private void init(int threadCount, Type type) {

        mThreadPool = Executors.newFixedThreadPool(threadCount);
        mPollingSemaphore = new Semaphore(threadCount);
        mTaskList = new LinkedList<Runnable>();
        if (type == null) {
            mType = Type.LIFO;
        } else {
            mType = type;
        }

        // 开启轮询线程
        mPollingThead = new Thread() {
            @Override
            public void run() {
                Looper.prepare();
                mPollingHanler = new Handler() {
                    @Override
                    public void handleMessage(Message msg) {
                        mThreadPool.execute(getTask());
                        try {
                            mPollingSemaphore.acquire();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                };
                mSemaphore.release();   // 释放一个信号量
                Looper.loop();
            }
        };
        mPollingThead.start();
    }

    /**
     * 添加一个任务
     * @param task   任务
     */
    private synchronized void addTask(Runnable task) {
        try {
            // mPollingHanler为空时,请求信号量,因为mPollingHanler创建完成会释放一个信号量
            if (mPollingHanler == null) mSemaphore.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mTaskList.add(task);

        mPollingHanler.sendEmptyMessage(0x110);
    }

    /**
     * 取出一个任务
     * @return 需要执行的任务
     */
    private synchronized Runnable getTask() {
        if (mType == Type.LIFO) {
            return mTaskList.removeLast();
        } else if (mType == Type.FIFO) {
            return mTaskList.removeFirst();
        }
        return null;
    }

    /**
     * 执行自定义的任务
     */
    public void doTask(Runnable runnable) {
        addTask(runnable);
    }

    public void releaseSemaphore() {
        mPollingSemaphore.release();
    }
}

调用

private TaskDispatcher td;

....


        for (int i = 0; i < modelList.size(); i++) {
            td = TaskDispatcher.getInstance();
            td.doTask(new Runnable() {
                @Override
                public void run() {
                    
                    ...

                    //这句必须有,任务处理完成后释放一个信号量,初始化时候可以指定线程数,如果为1的,下面这句不用
                    //                    td.releaseSemaphore();
                }
            });
        }

1.2、 手动从交互上控制

监听列表滚动,当列表滚动时候,禁止图片的加载,滚动停止时候,开始加载图片

1.2.1、 android 端 – picasso

android中使用picasso处理图片加载,可以用tag标记(pauseTag,resumeTag)处理。在滑动时候禁止图片加载,滑动停止启用图片加载。这个效果很明显,滑动不再卡了

场景一: 比如一个照片流列表,当我们快速滑动列表浏览照片的时候,后台会一直发起请求加载照片的,这可能会导致卡顿,那么我们就可以为每个请求设置一个相同的Tag,在快速滑动的时候,调用pauseTag暂停请求,当滑动停止的时候,调用resumeTag恢复请求,这样的体验是不是就会更好一些呢。

Adapter中添加如下代码:

Picasso.with(this).load(mData.get(position))
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .tag("PhotoTag")
                .into(holder.mImageView);

Activity中为RecyclerView添加滑动监听:

mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                final Picasso picasso = Picasso.with(MainActivity.this);

                if (newState == SCROLL_STATE_IDLE) {
                    picasso.resumeTag("PhotoTag");
                } else {
                    picasso.pauseTag("PhotoTag");
                }
            }
        });

场景二: 比如一个照片流列表界面,在弱网环境下,加载很慢,退出这个界面时可能会有很多请求没有完成,这个时候我们就可 以通过tag 来取消请求了。

@Override
    protected void onDestroy() {
        super.onDestroy();
        Picasso.with(this).cancelTag("PhotoTag");
    }

1.2.2、 ios 端

基于监听Y,来算速度,然后再controller里面设置一个全局变量,当速度低于多少的时候把这个全局变量布尔值打开,然后再绘制cell的时候根据这个全局变量去选择是否使用Sd去加载图片

2、 列表cell/item复用导致图片错乱解决方案

解决方案:

1:占位图

2:复用视图设置tag。以图片url为tag

2.1、 android 端

问题原理:
RecyclerView,包括之前用的ListView都存在这个问题。因为有ViewHolder的重用机制,每一个item在移除屏幕后都会被重新使用以节省资源,避免滑动卡顿。而在图片的异步加载过程中,从发出网络请求到完全下载并加载成Bitmap的图片需要花费很长时间,而这时候很有可能原先需要加载图片的item已经划出界面并被重用了。而原先下载的图片在被加载进ImageView的时候没有判断当前的ImageView是不是原先那个要求加载的,故可能图片被加载到被重用的item上,就产生了图片错位的问题。

解决方案:

解决思路也很简单,就是在下载完图片,准备给ImageView装上的时候检查一下这个ImageView

recyclerview,item,viewholder:设置hint图,给view设置tag

解决思路:

开始->当前item1给ImageView打tag
->当前item1发起网络请求
->异步处理网络请求
->当前item1滑出屏幕
->划出屏幕的item重用并滑进屏幕命名为item2
->item2重新给ImageView打上tag
->item1的图片下载完成,因为重用的原因,图片将要加载给item2
-> 用原先传入的item1设置的tag和新覆盖的tag比较,发现不相同
->不给当前item2设置图片,避免了图片错位

2.2、 ios 端

同理android,其实ios也可以设置tag

ios中一般用的都是sdwebimage。早年时候,sd加载图片会出现闪烁的情况。很棘手的问题,原因就是缓存的operation在前一个请求刚加载完成显示图片的时候,后一个请求开始了。。sd做了处理,就是在每次请求下载图片请求时候,都会将其哪一个请求cancle掉。

一般有4中方案:

1:不用cell复用方案

问题显而易见,就是cell数量大的话,内存是个问题。当然,我们在入门ios时候,第一次学习tableview使用的时候用的也是这种。当你觉得这种很水的时候,说明你现在也已经很溜了

- (UITableViewCell*)dequeueReusableCellWithIdentifier:(NSString*)identifier 
换为
-(UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath

2:特定重用标识方案

采用cell复用策略。但是在初始化cell的时候,identifier指定为唯一确定cell的字符串,比如:cell的section + row。这种方式的复用,也就是相当于cell自我复用。这种方案相对于第一种有进步,但是依然很不合理。

- (UITableViewCell*)dequeueReusableCellWithIdentifier:(NSString*)identifier 

3:cell复用,从新初始化子视图方案

思路就是,采用cell复用。但是拿到复用的cell后,将cell的所有自定义子视图移除,初始化数据需要的视图。这种方案,相对于前两种,进步挺大。但是深入想一想就知道喽。频繁的初始化视图,渲染。cpu,gpu累不累。如果你要是用的masonry约束。那完了。

4:cell复用,根据数据动态调整cell子视图方案

前面三种方案,如果是针对简单的cell视图结构,数据量又不大,采用哪种都可以实现,也没有什么差别。只是直观的体现了你的水准而已,嘿嘿。

应用场景:比如微信,做两天界面时候,cell类型比较多。文本,图片,语音,视频,地图等。这种cell,如果要柔和到一个cell里,里面的逻辑控制会写死人的。我处理这块的时候,经历过三个阶段:

1):收发cell是一个cell。用的还是autolayout。当时cell比较简单,就文本,图片,语音。但是已经哭死了。这种写法:逻辑不好控制,并且不好适配

2):后来约束用了masonry。cell也分开了。receiveCell,sendCell。这种方案现在想想也是水啊。每个cell中依然柔和很多类型数据,健壮性一点都不好,特别是sendCell,有好多种临时视图,比如发送indicator,语音下载indicator,语音播放视图,发送失败视图等。单masonry控制视图状态就够累了

3):前面两种方案都不好,最后重构了。还是masonry约束视图。根据消息类型分cell类型。比如:receiveTextCell,sendAudioCell等。每类型cell只控制自己的东西,并且不会有太大变化,毕竟语音cell处理的都是语音的视图/数据嘛。并且对于那些临时视图,采用懒加载。masonry约束他们也不采用remake(通知系统刷新约束,麻烦)。直接update。瞬间清爽了。代码可读性,拓展性,稳定性都有了

这种思路在android中体现也比较好。关于listview/recyclerview,有个getItemViewType,就可以根据item数据类型不同,进行分类。比如BaseRecyclerViewAdapterHelper库。完美契合这种情况

    原文作者:DaZenD
    原文地址: https://www.jianshu.com/p/0bae27ff217f
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞