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优化应用的性能,对于一些小问题并没有做处理:如没有移除观察者,没有使定时器在合适的时机失效等,在实际项目中必须做出相应的处理!

posted @ 2017-01-19 11:35  恋~时光  阅读(1274)  评论(0编辑  收藏  举报