RunLoop应用之性能优化
RunLoop介绍
昨天听了一节潭州iOS的公开课,内容是如何使用RunLoop来优化iOS应用的性能,感觉还不错,所以就在这里写一篇文章,谈谈自己的理解。
众所周知,iOS应用启动后,不会正常的自动退出。这就是因为iOS系统拥有的RunLoop机制的作用,当应用启动后,主线程的RunLoop默认开启,从而应用进入一个死循环,阻止了程序在运行完毕之后退出。
RunLoop在循环过程中监听着port事件和timer事件,当前线程有任务时,唤醒当当线程去执行任务,任务执行完成以后,使当前线程进入休眠状态。
问题
简单介绍RunLoop后,我们谈谈今天要解决的问题。
我们都知道,为了优化用户体验,在iOS开发过程中,和UI相关任务放在主线程,和UI无关的比较耗时的操作放在子线程。那么问题来了,当和UI相关又十分耗时的任务如何处理呢?
实例
向一个UITableView上加载图片,当图片很大,每一行显示图片也比较多的时候,在滑动的过程中就会十分的卡顿。因为界面的一次渲染是在一次RunLoop中完成的,当图片很大,显示的数量也比较多的时候,图片渲染就会相当耗时,这时候一次RunLoop的运行时间就会很长。而在这个过程中,用户的交互事件得不到处理,因此会造成拖动事件的卡顿。下载原始工程
解决问题
对于实例中的问题,我们想到的解决方案是:有没有办法,使得RunLoop的单次运行时间变短,当接收到用户的交互事件时,可以很快响应用户的交互,当用户的交互完成后,我们继续处理未完成的任务?答案当然是肯定的。
1、任务分割
为了缩短RunLoop的单次运行时间,我们将图片分为多次渲染,即:一次RunLoop渲染一张图片。
修改代码如下:
删除如下代码的注释
@property (nonatomic, strong) NSMutableArray *tasks;
@property (nonatomic, assign) NSInteger maxTaskNumber;
删除 - (void)viewDidLoad中相关注释
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. // [self addRunloopOvserver]; self.maxTaskNumber = 20; self.tasks = [NSMutableArray array]; // self.timer = [NSTimer scheduledTimerWithTimeInterval:0.01 repeats:YES block:^(NSTimer * _Nonnull timer) { // NSLog(@"timer"); // }]; }
在
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath中创建任务
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"identity" forIndexPath:indexPath]; for (int i = 1; i < 4; i++) { UIImageView *imageView = [cell.contentView viewWithTag:i]; [imageView removeFromSuperview]; } for (int i = 1; i < 4; i++) { // CGFloat leading = 10, space = 20, width = 103, height = 87, top = 15; // UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake((i - 1) * (width + space) + leading, top, width, height)]; // [cell.contentView addSubview:imageView]; // imageView.tag = i; // imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"]]; void(^task)() = ^{ CGFloat leading = 10, space = 20, width = 103, height = 87, top = 15; UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake((i - 1) * (width + space) + leading, top, width, height)]; [cell.contentView addSubview:imageView]; imageView.tag = i; imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"]]; }; [self.tasks addObject:task]; if (self.tasks.count == self.maxTaskNumber) { [self.tasks removeObjectAtIndex:0]; } } return cell; }
2、在适当的时机添加并执行任务
在什么时候添加执行任务呢,我们需要RunLoop每次运行执行一次任务,若果我们知道RunLoop的运行开始或者运行结束时机,这时候无非是最好的加载任务时机。
3、获取RunLoop的运行结束的时机(运行开始的时机)
RunLoop的状态变化,我们是可以通过注册观察者来监听的,下面我们注册观察者
取消以下代码的注释,添加观察者
- (void)addRunloopOvserver{ CFRunLoopRef runloop = CFRunLoopGetCurrent(); CFRunLoopObserverContext context = {0, (__bridge void *)self, &CFRetain, &CFRelease}; // callBack是回调的函数指针 CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &callBack, &context); CFRunLoopAddObserver(runloop, observer , kCFRunLoopDefaultMode); CFRelease(observer); }
取消以下代码的注释,添加回调函数,在回调函数中执行一次渲染任务
void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){ ViewController *VC = (__bridge ViewController *)(info); if (VC.tasks.count) { void(^task)() = [VC.tasks firstObject]; if (task) { task(); } [VC.tasks removeObject:task]; } }
取消- (void)viewDidLoad中添加观察者的注释
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. [self addRunloopOvserver]; self.maxTaskNumber = 20; self.tasks = [NSMutableArray array]; // self.timer = [NSTimer scheduledTimerWithTimeInterval:0.01 repeats:YES block:^(NSTimer * _Nonnull timer) { // NSLog(@"timer"); // }]; }
现在,当kCFRunLoopDefaultMode类型的RunLoop执行完成,线程即将进入休眠时,就会回调我们的回调函数:caooBack,从而执行一次渲染任务
4、执行任务
到第3部为止,每当一个RunLoop运行循环一次,就会调用一次回调函数,解决了拖动卡顿的问题。但是我们会发现,很多图片不会被加载,这是因为当任务执行完成之后,RunLoop如果没有监听到要处理的事件,就会让线程进入睡眠,从而终止下一次的渲染任务。为了解决这个问题,我们添加一个定时器,让RunLoop不断能过监听到事件,从而不断的回调callBack函数。
取消如下注释,添加定时器
@property (nonatomic, strong) NSTimer *timer;
取消- (void)viewDidLoad中添加定时器的注释
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. [self addRunloopOvserver]; self.maxTaskNumber = 20; self.tasks = [NSMutableArray array]; self.timer = [NSTimer scheduledTimerWithTimeInterval:0.01 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"timer"); }]; }
运行一下,发现问题已经得到解决,拖动完全不卡顿,并且能够在拖动结束后加载图片。
思考
上面的处理方法解决了我们遇到的问题,但是对于我这种能一行代买搞定的事情,绝不写两行代码的lazy man来说,感觉是否略显复杂了,里面还涉及到了CoreFoundation框架中的函数,这些C函数写起来就是蛋疼啊。那么有什么更好的处理方式呢?
我们会想上面解决方式中添加定时器是为了让RunLoop不断监听到timer事件,从而不断进行图片的渲染处理,也就是说,定时器每运行一次,就会执行一次图片的渲染任务,而定时器内上面事情都没有处理。那么,我们把处理图片渲染的任务放到定时器中处理,是不是会得到一样的效果呢?这样就只需要添加一个定时器,并在定时器中不断执行任务,无需再监听什么RunLoop的状态变化。
注释掉添加监听的相关方法,并在定时器中添加对图片渲染任务的处理
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. // [self addRunloopOvserver]; self.maxTaskNumber = 20; self.tasks = [NSMutableArray array]; self.timer = [NSTimer scheduledTimerWithTimeInterval:0.01 repeats:YES block:^(NSTimer * _Nonnull timer) { void(^task)() = [self.tasks firstObject]; if (task) { task(); } [self.tasks removeObject:task]; }]; }
//- (void)addRunloopOvserver{ // CFRunLoopRef runloop = CFRunLoopGetCurrent(); // CFRunLoopObserverContext context = {0, (__bridge void *)self, &CFRetain, &CFRelease}; // CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &callBack, &context); // CFRunLoopAddObserver(runloop, observer , kCFRunLoopDefaultMode); // CFRelease(observer); //}
//void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){ // ViewController *VC = (__bridge ViewController *)(info); // if (VC.tasks.count) { // void(^task)() = [VC.tasks firstObject]; // task(); // [VC.tasks removeObject:task]; // } //}
运行一下,发现和前面的解决方式完全一样,问题得到解决。
总结
本文通过一个实例介绍了RunLoop在项目中的应用,通过对RunLoop的理解,想到解决问题的方法。然后进一步思考出更为简单的处理方式。但需要注意的是,思考后得到的处理方式和添加观察者的处理方式在本质上是相同的,都是让RunLoop不断监听到timer事件,然后在监听到事件后处理一次不消耗太多事件的任务。
通过这个项目,我们应该知道,RunLoop的强大,并且学会利用RunLoop解决实际遇到的问题。还要善于思考,从而学到更好的解决方法!
注意
demo重在说明如何利用RunLoop优化应用的性能,对于一些小问题并没有做处理:如没有移除观察者,没有使定时器在合适的时机失效等,在实际项目中必须做出相应的处理!