解决tableview加载高清图片,滑动卡顿问题
产品今天给了个需求,最简单的tableview上展示数据,不过有个问题是给的图片都是高清的,所以滑动的时候不流畅,然后就去搜索,最后找到一个大神写的代码,通过runloop解决,感觉很不错,所以写篇文章记录一下。
runloop介绍
1,首先大家先了解一下runloop,网上一搜一大堆,我就简单说一下,大家都知道runloop的主要作用是不断的循环监听事件的,有事件发生时都会触发它,但是不同的事件触发它的模式不同(NSDefaultRunLoopMode和NSRunLoopCommonModes最常用的模式),时间计时器,网络请求会触发它的NSDefaultRunLoopMode,交互(点击,滑动)会触发NSRunLoopCommonModes,NSRunLoopCommonModes比NSDefaultRunLoopMode优先级更高,我们平常应该遇到过,当你滑动tableview的时候你的时间计时器就会停止,所以你就会把你的时间计时器切换到NSRunLoopCommonModes。(这里需要用代码提醒一下,把时间计时器切换到NSRunLoopCommonModes会带来哪些坏处)。
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)test{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"睡一秒钟");
}
我们可以看到在test的方法里面有个耗时操作,这时候当你滑动tableview的时候,就会卡顿,因为它们都处于NSRunLoopCommonModes模式,没有优先级之分了。所以用NSRunLoopCommonModes的时候要注意有没有耗时操作。
2,runloop还有一个功能就是,每次循环都会对你界面上的ui绘制一遍,主要是速度快,我们看不出来,所以当界面中出现高清的图片时因为绘制的慢,就会导致卡顿。
3,对tableview性能优化一般有:加载耗时操作放子线程,更新ui放主线程,说到这里,我们提一下,为什么更新ui放主线程,可能有些人只知道更新ui放主线程,不知道为什么,ui都是UIKit框架的东西,为了增加效率,苹果不建议定义它的(ui的一些类)属性的时候加线程锁的,我们都会用nonatomic,非原子性的,它不是线程安全的,所以把更新ui放到主线程,防止出现多个线程访问它,出现问题。
操作不流畅的代码
下面问题来了,也是我们今天要讲的,如果更新ui也是耗时操作,就像在2提到的,绘制高清图片,那咱们就先来一段卡顿的代码
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UITableView *tb = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
tb.delegate = self;
tb.dataSource = self;
[self.view addSubview:tb];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 200;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return 85;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
NSString *identifier = @"cell";
UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:identifier];
if(cell==nil){
cell=[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
for (UIView *subView in cell.contentView.subviews) {
[subView removeFromSuperview];
}
UILabel *contentLb = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 320, 40)];
contentLb.font = [UIFont systemFontOfSize:15];
contentLb.textColor = [UIColor grayColor];
contentLb.numberOfLines = 2;
contentLb.lineBreakMode = NSLineBreakByClipping;
contentLb.text = @"网络是由节点和连线构成,表示诸多对象及其相互联系。在数学上,网络是一种图,一般认为";
[cell.contentView addSubview:contentLb];
NSString *path = [[NSBundle mainBundle] pathForResource:@"spaceship" ofType:@"jpg"];
UIImage *image = [UIImage imageWithContentsOfFile:path];
UIImageView *iv1 = [[UIImageView alloc] initWithFrame:CGRectMake(0, contentLb.frame.origin.y+contentLb.frame.size.height, 80, 40)];
iv1.image = image;
[cell.contentView addSubview:iv1];
UIImageView *iv2 = [[UIImageView alloc] initWithFrame:CGRectMake(iv1.frame.origin.x+iv1.frame.size.width, contentLb.frame.origin.y+contentLb.frame.size.height, 80, 40)];
iv2.image = image;
[cell.contentView addSubview:iv2];
UIImageView *iv3 = [[UIImageView alloc] initWithFrame:CGRectMake(iv2.frame.origin.x+iv2.frame.size.width, contentLb.frame.origin.y+contentLb.frame.size.height, 80, 40)];
iv3.image = image;
[cell.contentView addSubview:iv3];
return cell;
}
paceship.jpg这张图片我用的差不多有3m,这时候会有稍微的不流畅,还不是那么明显,你可以用个更大的试试。那么我们怎么优化这段代码呢。
优化之后代码
1,因为我们拖拽tableview,runloop就要循环一次,对我们的ui进行绘制一遍,由于图片太大,绘制时间长,出现卡顿,上面我们说过,拖拽的时候runloop处于NSRunLoopCommonModes模式,如果在这个模式下我们不让他绘制图片,等他处于NSDefaultRunLoopMode(拖拽完后他就会回到NSDefaultRunLoopMode模式),我们再让它绘制图片是不是就不会卡顿了,这时候我们就要对runloop进行监听了。给它添加观察者,下面看代码实现
#import "ViewController.h"
#import <objc/runtime.h>
//定义一个block,用来存放加载图片的事件typedef BOOL(^RunloopBlock)(void);
@interface ViewController ()
/** 定义一个数组,存放加载图片的事件 */
@property (nonatomic,strong) NSMutableArray *tasks;
/** 最大任务数,因为我们一个屏幕可能就显示20张图片,这个数不确定,看你自己设置cell的高度了,如果你一个cell上放2个图片,一个屏幕上就能看到2个cell,那你的最大数就是4 */
@property(assign,nonatomic)NSUInteger max;
/**添加一个timer,可以一直让runloop处于唤醒状态,并且处于NSDefaultRunLoopMode模式,这样就可以不断绘制图片*/
@property(nonatomic,strong)NSTimer * timer;
@end
@implementation ViewController
- (void)_timerFiredMethod{
}
- (void)viewDidLoad {
[super viewDidLoad];
_max = 28;//我设置的cell高度,一个屏幕能显示28张图片
_tasks = [NSMutableArray array];
_timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(_timerFiredMethod) userInfo:nil repeats:YES];
UITableView *tb = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
tb.delegate = self;
tb.dataSource = self;
[self.view addSubview:tb];
//注册监听
[self addRunloopObserver];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 200;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return 85;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
NSString *identifier = @"cell";
UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:identifier];
if(cell==nil){
cell=[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
for (UIView *subView in cell.contentView.subviews) {
[subView removeFromSuperview];
}
UILabel *contentLb = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 320, 40)];
contentLb.font = [UIFont systemFontOfSize:15];
contentLb.textColor = [UIColor grayColor];
contentLb.numberOfLines = 2;
contentLb.lineBreakMode = NSLineBreakByClipping;
contentLb.text = @"网络是由节点和连线构成,表示诸多对象及其相互联系。在数学上,网络是一种图,一般认为";
[cell.contentView addSubview:contentLb];
//不要直接加载图片!! 你将加载图片的代码!都给RunLoop!!
[self addTask:^BOOL{
NSString *path = [[NSBundle mainBundle] pathForResource:@"spaceship" ofType:@"jpg"];
UIImage *image = [UIImage imageWithContentsOfFile:path];
UIImageView *iv1 = [[UIImageView alloc] initWithFrame:CGRectMake(0, contentLb.frame.origin.y+contentLb.frame.size.height, 80, 40)];
iv1.image = image;
[cell.contentView addSubview:iv1];
return YES;
}];
[self addTask:^BOOL{
NSString *path = [[NSBundle mainBundle] pathForResource:@"spaceship" ofType:@"jpg"];
UIImage *image = [UIImage imageWithContentsOfFile:path];
UIImageView *iv2 = [[UIImageView alloc] initWithFrame:CGRectMake(80, contentLb.frame.origin.y+contentLb.frame.size.height, 80, 40)];
iv2.image = image;
[cell.contentView addSubview:iv2];
return YES;
}];
[self addTask:^BOOL{
NSString *path = [[NSBundle mainBundle] pathForResource:@"spaceship" ofType:@"jpg"];
UIImage *image = [UIImage imageWithContentsOfFile:path];
UIImageView *iv3 = [[UIImageView alloc] initWithFrame:CGRectMake(160, contentLb.frame.origin.y+contentLb.frame.size.height, 80, 40)];
iv3.image = image;
[cell.contentView addSubview:iv3];
return YES;
}];
return cell;
}
//添加任务
- (void)addTask:(RunloopBlock)unit{
[self.tasks addObject:unit];
//保证之前没有显示出来的任务,不再浪费时间加载
if (self.tasks.count > self.max) {
[self.tasks removeObjectAtIndex:0];
}
}
//添加监听,用来监听runloop
- (void)addRunloopObserver{
//获取当前的RunLoop
CFRunLoopRef runloop = CFRunLoopGetCurrent();//用CFRunLoopGetCurrent()和CFRunLoopGetMain()都一样,因为我们现在操作都是在主线程,这个方法就是得到主线程的runloop,因为每个线程都有一个runloop
//定义一个观察者,这是一个结构体
CFRunLoopObserverContext context = {
0,
(__bridge void *)(self),
&CFRetain,
&CFRelease,
NULL
};
//定义一个观察者
static CFRunLoopObserverRef defaultModeObsever;
//创建观察者
defaultModeObsever = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, NSIntegerMax - 999, &Callback, &context);//kCFRunLoopBeforeWaiting观察runloop等待的时候就是处于NSDefaultRunLoopMode模式的时候,YES是否重复观察,Callback回掉方法,就是处于NSDefaultRunLoopMode时候要执行的方法,其他参数我也不知道什么意思
//添加当前RunLoop的观察者
CFRunLoopAddObserver(runloop, defaultModeObsever, kCFRunLoopDefaultMode);
//c语言有creat 就需要release
CFRelease(defaultModeObsever);
}
static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
ViewController * vc = (__bridge ViewController *)(info); //这个info就是我们在context里面放的self参数
if (vc.tasks.count == 0) {
return;
}
BOOL result = NO;
while (result == NO && vc.tasks.count) {
//取出任务
RunloopBlock unit = vc.tasks.firstObject;
//执行任务
result = unit();
//干掉第一个任务
[vc.tasks removeObjectAtIndex:0];
}
}
@end
这是改装后的代码