iOS开发攻城狮的集散地UI自我学习材料

解决tableview加载高清图片,滑动卡顿问题

2017-07-26  本文已影响446人  pengmengli

产品今天给了个需求,最简单的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

这是改装后的代码

上一篇下一篇

猜你喜欢

热点阅读