美文网首页iOS开发
OC-Run Loop的理解和使用

OC-Run Loop的理解和使用

作者: 楚槟夕 | 来源:发表于2017-11-23 01:28 被阅读258次
    • Run Loop是什么

    RunLoop顾名思义,是运行循环。它跟线程是一一对应的,每一个线程都有一个RunLoop,在需要的时候创建。RunLoop的作用很简单,就是保持线程不会退出,并且处理一些事件。

    如果没有RunLoop,线程只要一执行完代码就会退出。RunLoop类似一个while循环,但是又不像while循环会占用CPU资源,RunLoop在等待的时候处于休眠状态,只有接收到事件时,才会被唤醒,然后再做相应的处理。

    程序启动时,系统会自动为我们开启主线程的RunLoop,这就保证了我们的程序不会退出,并且可以一直响应我们的操作。而子线程的RunLoop并没有开启,需要我们手动开启。

    • Run Loop使用

    说到使用RunLoop,其实我们在使用NSTimer的时候就已经使用过它了,只不过那时候并没有对RunLoop深入研究,我们来重新体验一下一个NSTimer的简单使用:

        //创建一个Timer
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"timer");
        }];
        
        //把它加到RunLoop里
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    

    一个NSTimer必须和RunLoop一起工作,不然它没办法运作。这里再给RunLoop添加timer的时候,有一个参数叫Mode,这是RunLoop模式,不同模式处理不同类型输入源的事件。

    1. NSDefaultRunLoopMode:App的默认Mode,通常主线程是在这个Mode下运行。
    2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
    3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
    4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
    5. NSRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode,它会同时处理默认模式和UI模式中的事件。

    到这里又引发出来了新的问题,为什么NSTimer必须添加到RunLoop才能使用呢?

    这就涉及到了RunLoop所能处理的事件了:

    Run loop接收输入事件来自两种不同的来源:输入源(input source)和定时源(timer source)。输入源传递异步事件,通常消息来自于其他线程或程序。定时源则传递同步事件,发生在特定时间或者重复的时间间隔。两种源都使用程序的某一特定的处理例程来处理到达的事件。

    这张官方的图简单的描述了RunLoop所能处理的事件来源:


    接下来,我们来玩一玩RunLoop。在OC中,有NSRunLoopCFRunLoop两种方式来获取并且操作RunLoop。其中NSRunLoopCFRunLoop的封装。我们以NSRunLoop为主对RunLoop进行使用。首先,我们创建一个线程,然后开启它的runloop,我们如何证明它的runloop已经开启呢?结合上图,我们只需要找一个runloop能够处理的事件,然后让它去处理就可以了,我这里挑选了- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait ;方法:

    //首先持有一个线程对象,方便我们之后使用它
    @property (nonatomic, strong) NSThread *thread;
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        //初始化并开启,在线程内部开启它的runloop
        self.thread = [[NSThread alloc] initWithBlock:^{
            NSLog(@"这是一条子线程%@",[NSThread currentThread]);
            
            [[NSRunLoop currentRunLoop] run];
        }];
        [self.thread start];
    }
    
    //点击屏幕时,在线程上执行下面的打印
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
    }
    
    //打印出线程,以便我们确认是同一条
    - (void)test{
        NSLog(@"哈哈哈%@",[NSThread currentThread]);
    }
    

    我们可以通过[NSRunLoop currentRunLoop]获取当前线程的RunLoop,开启RunLoop只需要调用其run方法。

    按照我们预想的结果,运行以后每次点击屏幕都应该有输出,但是实际上我们点击屏幕并没有任何效果。这是因为开启RunLoop之前必须给其指定至少一种输入源或者定时源,不然开启之后会马上退出。说到这里,我们得看一下RunLoop在一次循环的周期内,到底做了什么事情:

    每次运行run loop,你线程的run loop对会自动处理之前未处理的消息,并通知相关的观察者。具体的顺序如下:

    1. 通知观察者run loop已经启动
    2. 通知观察者任何即将要开始的定时器
    3. 通知观察者任何即将启动的非基于端口的源
    4. 启动任何准备好的非基于端口的源
    5. 如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9。
    6. 通知观察者线程进入休眠
    7. 将线程置于休眠直到任一下面的事件发生:
      • 某一事件到达基于端口的源
      • 定时器启动
      • Run loop设置的时间已经超时
      • run loop被显式唤醒
    8. 通知观察者线程将被唤醒。
    9. 处理未处理的事件
      • 如果用户定义的定时器启动,处理定时器事件并重启run loop。进入步骤2
      • 如果输入源启动,传递相应的消息
      • 如果run loop被显式唤醒而且时间还没超时,重启run loop。进入步骤2
    10. 通知观察者run loop结束。

    从以上我们可以知道,如果是定时器事件,执行之后会直接重启RunLoop,如果是其它事件,处理完毕后,不会再次唤醒RunLoop,要想它继续监听事件,我们必须得手动唤醒它。之前在我们点击的时候,runloop已经退出了,所以代码并没有执行。

    不过这难不倒我们,我们可以给它一个循环,让它不断得开启:

        while (true) {
            [[NSRunLoop currentRunLoop] run];
        }
    

    再次运行,点击屏幕就可以看到打印出的信息:

    开启RunLoop还有另外几个方法,我们平时最好不要直接使用run方法,可能会造成无限循环:

    //同run方法,增加超时参数limitDate,避免进入无限循环。使用在UI线程(亦即主线程)上,可以达到暂停的效果。
    (void)runUntilDate:(NSDate *)limitDate; 
    
    //等待消息处理,好比在PC终端窗口上等待键盘输入。一旦有合适事件(mode相当于定义了事件的类型)被处理了,则立刻返回;类同run方法,如果没有事件处理也立刻返回;有否事件处理由返回布尔值判断。同样limitDate为超时参数。
    (BOOL)runMode:(NSString )mode beforeDate:(NSDate )limitDate;
    

    以上是一些简单的操作,我们可以利用RunLoop去监测一些事件,当它发生的时候再去做处理。但是用while循环会让CPU一直在工作,所以我们最好设置一种终止RunLoop循环的条件。

    前面提到,RunLoop在开启时,需要给它指定输入源,而输入源是可以自定义的,不过它需要使用CFRunLoop。接下来我们可以自己自定义一种输入源:

    /* Run Loop Source Context的三个回调方法,其实是C语言函数 */
    
    // 当把当前的Run Loop Source添加到Run Loop中时,会回调这个方法。
    void runLoopSourceScheduleRoutine (void *info, CFRunLoopRef runLoopRef, CFStringRef mode)
    {
        NSLog(@"Input source被添加%@",[NSThread currentThread]);
    
    }
    
    // 当前Input source被告知需要处理事件的回调方法
    void runLoopSourcePerformRoutine (void *info)
    {
        NSLog(@"回调方法%@",[NSThread currentThread]);
    }
    
    // 如果使用CFRunLoopSourceInvalidate函数把输入源从Run Loop里面移除的话,系统会回调该方法。
    void runLoopSourceCancelRoutine (void *info, CFRunLoopRef runLoopRef, CFStringRef mode)
    {
        NSLog(@"Input source被移除%@",[NSThread currentThread]);
    }
    
    //创建两个属性来保存`runLoopSource `和`runLoop `
    @implementation ViewController{
        CFRunLoopSourceRef runLoopSource;
        CFRunLoopRef runLoop;
    }
    
            //在之前的线程代码中为RunLoop添加Source
            self.thread = [[NSThread alloc] initWithBlock:^{
            NSLog(@"这是一条子线程%@",[NSThread currentThread]);
            
            runLoop = CFRunLoopGetCurrent();
            
            CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL,
                &runLoopSourceScheduleRoutine,
                &runLoopSourceCancelRoutine,
                &runLoopSourcePerformRoutine};
            
            //CFAllocatorRef内存分配器,默认NULL,CFIndex优先索引,默认0,CFRunLoopSourceContext上下文
            runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
            CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
            
            [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
    
        }];
        [self.thread start];
    

    然后我们需要在点击的时候通知InputSource,并且唤醒runLoop

        //通知InputSource
        CFRunLoopSourceSignal(InputSource);
        //唤醒runLoop
        CFRunLoopWakeUp(runLoop);
    

    然后点击测试一下:

    因为我们设置了超时时间,所以10秒以后,RunLoop就会退出。同时它的InputSource被自动移除。

    以上,我们简单的自己创建并添加了RunLoop的InputSource,实际开发中,我们可以对InputSource进行封装,使用起来更方便。这里就不做这一步了,网上可以找到比较完善的例子。

    RunLoop还有一个观察者,可以让我们监听到RunLoop的各种状态,它也需要用CFRunLoop来实现,接下来,我们在上面的基础上,对RunLoop添加观察者进行监听:

    // RunLoop监听回调
    void currentRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
    {
        NSString *activityDescription;
        switch (activity) {
            case kCFRunLoopEntry:
                activityDescription = @"kCFRunLoopEntry";
                break;
            case kCFRunLoopBeforeTimers:
                activityDescription = @"kCFRunLoopBeforeTimers";
                break;
            case kCFRunLoopBeforeSources:
                activityDescription = @"kCFRunLoopBeforeSources";
                break;
            case kCFRunLoopBeforeWaiting:
                activityDescription = @"kCFRunLoopBeforeWaiting";
                break;
            case kCFRunLoopAfterWaiting:
                activityDescription = @"kCFRunLoopAfterWaiting";
                break;
            case kCFRunLoopExit:
                activityDescription = @"kCFRunLoopExit";
                break;
            default:
                break;
        }
        NSLog(@"Run Loop activity: %@", activityDescription);
    }
    
            //为runLoop添加观察者
            CFRunLoopObserverContext  runLoopObserverContext = {0, NULL, NULL, NULL, NULL};
            CFRunLoopObserverRef    observer = CFRunLoopObserverCreate(NULL,//内存分配器,默认NULL
                                                                       kCFRunLoopAllActivities,//监听所有状态
                                                                       YES,//是否循环
                                                                       0,//优先索引,一般为0
                                                                       &currentRunLoopObserver,//回调方法
                                                                       &runLoopObserverContext//上下文
                                                                       );
            if (observer)
            {
                CFRunLoopAddObserver(runLoop, observer, kCFRunLoopDefaultMode);
            }
    

    运行以后:

    上面,我们对Runloop的使用做了简单的分析,但是对我们好像还是没什么卵用。接下来,我们通过一个实际的案例来运用RunLoop,让它变成我们的法宝。

    • Run Loop的实际应用

    我们在实际开发中经常会遇到TableView中有大量的图片显示,在滑动过程中,能明显得感觉到卡顿。我们这里用TableView显示多张大图简单模拟一下:

        self.tableView = [[UITableView alloc] initWithFrame:self.view.frame];
        self.tableView.delegate = self;
        self.tableView.dataSource = self;
        self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
        
        [self.view addSubview:self.tableView];
    
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
        return 299;
    }
    
    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
        return 100;
    }
    
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        
        NSString *identifier = @"identifier";
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
        
        if (!cell) {
            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
            cell.selectionStyle = UITableViewCellSelectionStyleNone;
        }
    
        for (UIView *view in cell.subviews) {
            [view removeFromSuperview];
        }
        
        UIImageView *imageView1 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
            imageView1.frame = CGRectMake(10, 10, 100, 80);
            [cell addSubview:imageView1];
        
        UIImageView *imageView2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
            imageView2.frame = CGRectMake(120, 10, 100, 80);
            [cell addSubview:imageView2];
        
        UIImageView *imageView3 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
            imageView3.frame = CGRectMake(230, 10, 100, 80);
            [cell addSubview:imageView3];
        
        return cell;
    }
    

    那么怎么做优化呢?利用我们前面了解的RunLoop可以实现这个优化。我们知道卡顿的主要原因是因为加载大量大图是比较耗时的,而在主线程上处理耗时操作时,我们滑动或者点击屏幕就会有卡顿的感觉,因为在同一条线程上的任务只能串行执行。而我们滑动屏幕时,一瞬间要显示很多张图片,这就形成了一个耗时操作。

    经过思考,我们可以把这些图片在每一次runLoop循环中添加一张,这样的话,因为每次只添加一张图片,时间大大缩短,就不会有卡顿的感觉了。

    我们这里利用runLoop的观察者来监听每一次runloop循环,然后在监听事件里,添加一张图片。我们这里把添加图片当做任务放到一个数组里面,任务就是一个block,这样我们在回调里面只需要拿出任务执行就OK了:

    //定义一个任务
    typedef void(^RunLoopTask)(void);
    //用来存放任务的数组
    @property (nonatomic, strong) NSMutableArray<RunLoopTask> *tasks;
    //最大任务数量
    @property (nonatomic, assign) NSInteger maxTaskCount;
    
        //初始化数据
        self.maxTaskCount = 24;
        self.tasks = [NSMutableArray array];
    
    //添加任务到数组
    - (void)addTask:(RunLoopTask)task{
        
        [self.tasks addObject:task];
        
        //保证之前没来得及显示的图片不会再绘制
        if (self.tasks.count > _maxTaskCount) {
            [self.tasks removeObjectAtIndex:0];
        }
    }
    
    
    //添加任务
        [self addTask:^{
            UIImageView *imageView1 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
            imageView1.frame = CGRectMake(10, 10, 100, 80);
            [cell addSubview:imageView1];
        }];
        
        [self addTask:^{
            UIImageView *imageView2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
            imageView2.frame = CGRectMake(120, 10, 100, 80);
            [cell addSubview:imageView2];
        }];
        
    
        [self addTask:^{
            UIImageView *imageView3 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
            imageView3.frame = CGRectMake(230, 10, 100, 80);
            [cell addSubview:imageView3];
        }];
    
    - (void)addObserverToMainRunLoop{
        //为runLoop添加观察者
        CFRunLoopObserverContext  runLoopObserverContext = {0, (__bridge void *)(self), NULL, NULL, NULL};
        CFRunLoopObserverRef    observer = CFRunLoopObserverCreate(NULL,//内存分配器,默认NULL
                                                                   kCFRunLoopBeforeWaiting,//等待之前
                                                                   YES,//是否循环
                                                                   0,//优先索引,一般为0
                                                                   &currentRunLoopObserver,//回调方法
                                                                   &runLoopObserverContext//上下文
                                                                   );
        if (observer)
        {
            CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
        }
        CFRelease(observer);
    }
    
    // RunLoop监听回调
    static void currentRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
    {
        MyViewController *vc = (__bridge MyViewController *)info;
        if (vc.tasks.count == 0) {
            return;
        }
        RunLoopTask task = vc.tasks.firstObject;
        task();
        [vc.tasks removeObjectAtIndex:0];
    }
    
    

    然后再运行,就很流畅了。当然代码并不是完整代码,篇幅有限,只能把主要代码贴上来。
    总结一下,我们可以把耗时的大量UI操作利用RunLoop分解,使界面保持流畅。

    本文参考iOS多线程编程指南(三)Run Loop
    本文所涉及到所有的代码点击前往

    相关文章

      网友评论

        本文标题:OC-Run Loop的理解和使用

        本文链接:https://www.haomeiwen.com/subject/htagpxtx.html