RunLoop概念与使用

1、RunLoop概念

RunLoop顾名思义就是能够一直循环运行的机制。这种机制一般称为“消息循环机制”,其原理大体以下:html

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是线程安全的。git

2、NSRunLoopMode

咱们在使用NSRunLoop时,会常常须要设置其mode属性。常见的mode属性主要包括:NSDefaultRunLoopModeUITrackingRunLoopModeNSRunLoopCommonModesgithub

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

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

在主线程中设置TimerCADisplayLink,咱们一般都会设置为NSRunLoopCommonModes属性,表示在NSDefaultRunLoopModeUITrackingRunLoopMode状态下都会进行监听,避免滑动时,没法回调。异步

3、NSRunLoop的使用

  • NSTimer

能够尝试将NSRunLoopCommonModes改为NSDefaultRunLoopMode,那么timerFired:函数在scrollview滑动的时候,就不会被定时调用了,直到滑动中止。async

- (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方法并不会调用。为什么在子线程就没法生效呢?oop

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

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

4、RunLoop系列文章

参考资料

相关文章
相关标签/搜索