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