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 是不会的。

参考资料

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章