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

  • 相关阅读:
    说一下 JSP 的 4 种作用域?
    CSS jquery 以动画方式显示投票结果图表
    Python动画【偶尔玩玩,挺好】
    关于Python【社区版】爬取网站图片
    Java 发送短信验证码【网建平台】
    Android发送接收短信
    如何在Java面试中介绍项目经验?
    Java面试之项目介绍
    IntelliJ IDEA 如何清理缓存和重启
    java实现支付宝接口-支付流程
  • 原文地址:https://www.cnblogs.com/yueyuanyueyuan/p/6297694.html
Copyright © 2020-2023  润新知