阅读 433

RunLoop概念与使用

一、RunLoop概念

RunLoop顾名思义就是可以一直循环运行的机制。这种机制通常称为“消息循环机制”,其原理大致如下:

void loop() {
    initialize();
    while(!quit) {
        id msg = get_next_message();
        process_message(msg);
    }
}
复制代码

在iOS中,NSRunLoopCFRunLoopRef就是实现“消息循环机制”的对象。其实NSRunLoop本质是由CFRunLoopRef封装的,提供了面向对象的API,而CFRunLoopRef是一些面向过程的C函数API。两者最主要的区别在于:NSRunLoop是非线程安全的,意味着你不能用非当前线程去调用当前线程的NSRunLoop,否则会出现意想不到的错误(You should never try to call the methods of an NSRunLoop object running in a different thread)。而CFRunLoopRef是线程安全的。

二、NSRunLoopMode

我们在使用NSRunLoop时,会经常需要设置其mode属性。常见的mode属性主要包括:NSDefaultRunLoopModeUITrackingRunLoopModeNSRunLoopCommonModes

程序应用大部分情况下是处于NSDefaultRunLoopMode状态,只有当scrollView滑动时,主线程RunLoop会自动切换为UITrackingRunLoopMode状态。

不同的mode影响到我们设置的监听者(比如TimerCADisplayLink)是否会被回调。比如在主线程中,设置TimerNSDefaultRunLoopMode属性,当应用在滑动时,Timer的方法是不会被回调的,因为滑动过程中,RunLoop会切换为UITrackingRunLoopMode状态,而它只是监听了NSDefaultRunLoopMode状态。

在主线程中设置TimerCADisplayLink,我们通常都会设置为NSRunLoopCommonModes属性,表示在NSDefaultRunLoopModeUITrackingRunLoopMode状态下都会进行监听,避免滑动时,无法回调。

三、NSRunLoop的使用

  • NSTimer

可以尝试将NSRunLoopCommonModes改成NSDefaultRunLoopMode,那么timerFired:函数在scrollview滑动的时候,就不会被定时调用了,直到滑动停止。

- (void)startTimer {
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

- (void)timerFired:(NSTimer *)timer {
    NSLog(@"fired timer in %@", [NSDate date]);
}
复制代码
  • CADisplayLink
- (void)startDisplayLink {
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTick:)];
    [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)displayLinkTick:(CADisplayLink *)link {
    NSLog(@"tick display link in %@", [NSDate date]);
}
复制代码
  • performSelector:withObject:afterDelay:

这里看似并没有使用到NSRunLoop,但其实是它内部会创建一个Timer,并加Timer加入到当前线程对应的NSRunLoop中(This method sets up a timer to perform the aSelector message on the current thread’s run loop. )。

- (void)performSel {
    [self performSelector:@selector(performSelFired:) withObject:@"perform" afterDelay:3.0 inModes:@[NSRunLoopCommonModes]];
    NSLog(@"performSelector start in %@", [NSDate date]);
}

- (void)performSelFired:(NSString *)object {
    NSLog(@"performSelector with obj: %@ in %@", object, [NSDate date]);
}
复制代码
  • 在子线程中使用NSRunLoop
- (void)performInThread {
    __weak typeof(self) wSelf = self;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"performInThread start in %@", [NSDate date]);
        [wSelf performSelector:@selector(threadFired:) withObject:@"thread perform" afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
    });
}

- (void)threadFired:(NSString *)object {
    NSLog(@"performInThread with obj: %@ in %@", object, [NSDate date]);
}
复制代码

运行该代码,会发现threadFired方法并不会调用。为何在子线程就无法生效呢?

a. 线程和RunLoop是一一对应的,且互相独立,比如主线程对应mainRunLoop,而子线程也是有它自己所对应的RunLoop。 b. 主线程的RunLoop在应用启动的时候就开始run了,而子线程是需要主动调用其run方法来启动。

- (void)performInThread {
  __weak typeof(self) wSelf = self;
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"performInThread start in %@", [NSDate date]);
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [wSelf performSelector:@selector(threadFired:) withObject:@"thread perform" afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
        [runLoop run];
    });
}
复制代码

获取到子线程对应的RunLoop后,调用其run方法就可以看到threadFired被调用了。注意:RunLoop是无法主动被创建的,只能通过在currentRunLoopmainRunLoop获取到对应的RunLoop

假设在这里做一个修改,将[runLoop run];方法提前,如下:

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop run];
[wSelf performSelector:@selector(threadFired:) withObject:@"thread perform" afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
复制代码

修改后,会发现threadFired函数又无法被调用了。这又是什么原因?

图片来源

这时因为NSRunLoop是需要source event才会一直运行的,否则运行完会被终止。这里通常会有两种source event:a.异步事件,通常为addPortperformSelector:onThread方法;b.Timer事件,通常为addTimerperformSelector:afterDelay等方法。

所以,提前调用run方法时,RunLoop没有设置任何source event,所以会立即终止,而执行到下面的performSelector方法时,这时虽然设置了timer source,但RunLoop已经终止,自然也就无法响应了。

  • addPort

通过addPort方法可以使RunLoop监听某个端口的事件,从而保证其一直运行。

- (void)addPort {
    __weak typeof(self) wSelf = self;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"start run addPort in %@", [NSDate date]);
        wSelf.thread = [NSThread currentThread];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    });
    
    for (NSInteger i = 1; i <= 3; i ++) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * i * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"start receive port msg in %@", [NSDate date]);
            [wSelf performSelector:@selector(receiveMsg) onThread:wSelf.thread withObject:nil waitUntilDone:NO];
        });
    }
    
}

- (void)receiveMsg {
    NSLog(@"receive msg in thread in %@", [NSDate date]);
}
复制代码

这里通过注册NSMachPort端口,来保证该线程的RunLoop一直处于运行状态。

这里有个问题,NSRunLoop设置的modeNSDefaultRunLoopMode,那么是不是意味着当应用有scrollView滑动时,会导致无法响应?答案是不会!这里可能很容易产生一个误解:只有mode设置为NSRunLoopCommonModes,才能保证在scrollView滑动的情况下也会响应。其实是不对的,应该有个前提条件:主线程。因为只有mainRunLoop才会在滑动时,切换为UITrackingRunLoopMode,子线程中的RunLoop是不会的。

四、RunLoop系列文章

参考资料