美文网首页Android开发Android开发经验谈Android知识
关于图片加载优化相关方案 -- android,ios

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

作者: DaZenD | 来源:发表于2018-08-25 10:33 被阅读17次

    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库。完美契合这种情况

    相关文章

      网友评论

        本文标题:关于图片加载优化相关方案 -- android,ios

        本文链接:https://www.haomeiwen.com/subject/doiwiftx.html