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。瞬间清爽了。代码可读性,拓展性,稳定性都有了
作者:DaZenD
链接:https://www.jianshu.com/p/0bae27ff217f
共同学习,写下你的评论
评论加载中...
作者其他优质文章