目录

  1. 介绍
  2. 用途
  3. Runloop Mode
  4. Runloop Mode Item
  5. Run Loop 运行机制
  6. 自定义 input source

介绍

Run Loop 正如其字面意思一样,表示运行循环,可以理解为一个 do-while 循环。Run Loop 与线程密切相关,每个线程对应着自己的一个 Run Loop,但是除了主线程的 Run Loop 是默认开启之外,剩下的线程的 Run Loop 一开始是不存在的,需要手动通过 [NSRunLoop currentRunLoop]CFRunLoopGetCurrent() 获取当前线程的 Run Loop,这两个方法分别对应着 Foundation 框架和 Core Foundation 框架,它们的原理为,若当前线程对应的 Run Loop 存在的时候,直接返回该 Run Loop,当该线程当前不存在 Run Loop 时候,就创建当前线程的 Run Loop。 Foundation 框架的方法是对 Core Foundation 框架的方法做了一层封装,另外,NSRunloop 是非线程安全的,而 CFRunloopRef 是线程安全的。

用途

Run Loop 主要是用来保持线程常驻,从而接收事件的。比如常见的触摸事件,就是通过主线程的 Run Loop 来接收,然后再分发给相关的处理程序。

Runloop Mode

每一个 Run Loop 包含着多个 Runloop mode,每次运行的时候,它只会运行在一个特定的 Runloop mode 下,iOS 系统常用的 Runloop Mode 有两个:

  • kCFRunLoopDefaultMode (NSDefaultRunLoopMode)
  • UITrackingRunLoopMode。

另外还有一种特殊的commonModes:kCFRunLoopCommonModes(NSRunLoopCommonModes), 这种情况可以理解为别的 Mode 的集合,假如一个 Run Loop 运行在 UITrackingRunLoopMode 下,那么它不但会接收被标记为 UITrackingRunLoopMode 的事件, 也会接收被标记为 NSRunLoopCommonModes 的事件。我们可能会遇到这种情况,当一个 UIScrollView 在滑动的时候,主线程的 NSTimer 事件就不触发了,这种情况是因为当 UIScrollView 在滑动的时候,主线程的 Run Loop 就处于 UITrackingRunLoopMode 下,只接收被标记为 UITrackingRunLoopMode 和 NSRunLoopCommonModes 的事件, 而 NSTimer 默认被标记为 NSDefaultRunLoopMode 的,所以该 timer 事件就被丢弃了。解决的办法是通过

- (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode

来让此 timer 被标记为 NSRunLoopCommonModes 或 UITrackingRunLoopMode, 最好是被标记为 NSRunLoopCommonModes,这样,不管线程的 Run Loop 运行在什么 Mode 下,都能接收到事件了。

Runloop Mode Item

每一个 Runloop Mode 都包含若干 Mode Item,Mode Item 有3种,分别是 Source, Timer 和 Observer。每一个Run Loop 会运行在特定的 Mode 下,这个 Mode 里面必须包含至少一个 Mode Item,如果这个 Mode 里面没有包含任何 Mode Item,那么当前的 Run Loop 会立刻退出。我们可以通过以下方法来添加 Mode Item 到指定的 Mode 中:

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

通过下面的方法把 Mode Item 从指定 Mode 中移除

CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

Source

Source(CFRunLoopSourceRef) 用来产生事件的,它有两个版本 Source0 和 Source1。

Source0 包含了一个回调函数,它无法主动唤醒线程的 Run Loop,需要先使用CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

Source1 被称为基于 Port 的 source (Port based source); 它主要用来进行线程间的通信,能主动唤醒线程的 Run Loop。

Timer

Timer 就是我们平时使用的 NSTimer,它实际上是对 CFRunLoopTimerRef 进行了封装,当我们将其加入 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行对应的回调函数。

假如 Timer 被设置为只触发一次,其触发后会从Run Loop中移除,假如 Timer 被设置为循环触发时,会一直保存在当前Run Loop中,直到调用invalidated方法。

Observer

Run Loop 的 Observer(CFRunLoopObserverRef) 会观察 Run Loop 的运行状态,对于不同的状态执行不同的回调函数,Run Loop 的状态有以下几种:

  • Run Loop 进入的时候
  • Run Loop 即将处理一个Timer的时候
  • Run Loop 即将处理一个Input Source的时候
  • Run Loop 即将进入休眠的时候
  • Run Loop 刚从休眠中被被唤醒的时候
  • Run Loop 即将退出的时候

Observer 同 Timer 类似,在被设置为只触发一次,其触发后会从Run Loop中移除,假如被设置为循环触发时,会一直保存在当前Run Loop中。

Run Loop 运行机制

Run Loop 是用来接收事件的一个循环,其生命周期如下:

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

上述步骤中,2-9 处于一个 while 循环中。

自定义 input source

Source0

我们首先定义一个 Source:

CFRunLoopSourceContext context = {0,(__bridge void *)(self), NULL, NULL, NULL, NULL, NULL, NULL, NULL, &runLoopSourceFired};

self.inputSource = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);

其中 context 里面的最后一个参数表示 source 触发时候的回调函数:

void runLoopSourceFired(void *info)
{
    NSLog(@"source0 Fired");
}

之后我们建立并开启一个线程:

self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(p_threadWillEnter) object:nil];
[self.thread start];

在这个线程里我们执行如下方法:

- (void)p_threadWillEnter{

    @autoreleasepool {

        CFRunLoopRef currentRunloop = CFRunLoopGetCurrent();

        self.backgroundRunloop = currentRunloop;

        CFRunLoopAddSource(currentRunloop, self.inputSource, kCFRunLoopDefaultMode);//添加 source0 到当前线程的 Run Loop 里

        do {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];//开启 Run Loop
        } while (YES);
    }
}

之后假如有事件发生,我们需要通知辅助线程,执行 source0 回调:

- (IBAction)buttonDidClicked:(id)sender {

    CFRunLoopSourceSignal(self.inputSource); //将该 source 标记为待处理

    CFRunLoopWakeUp(self.backgroundRunloop); //唤醒辅助线程
}

这样,每按一下按钮,我们就会看到后台线程被唤醒,并执行了 source0 的回调,打印出了 “source0 Fired”。

Source1 (Port-based Source)

首先定义一个 NSPort

  self.mainThreadPort = [NSPort port];

之后设置处理该 port 接收事件的 delegate

   self.mainThreadPort.delegate = self;

把该 port 加入到 Run Loop 里,并开启 Run Loop

    [[NSRunLoop currentRunLoop] addPort:self.mainThreadPort forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];

实现 port 的 delegate 方法:

- (void)handlePortMessage:(NSPortMessage *)message{

    UInt32 msid = [message msgid];

    NSLog(@"msid == %d",msid);
}

创建新的线程和该线程的 Port

self.backgroundThread = [[NSThread alloc] initWithTarget:self selector:@selector(p_threadWillEnter) object:nil];
self.backgroundThreadPort = [NSPort port];
self.backgroundThreadPort.delegate = self;

开启该线程的 Run Loop

- (void)p_threadWillEnter{

    @autoreleasepool {

        NSRunLoop *runloop = [NSRunLoop currentRunLoop];

        [runloop addPort:self.backgroundThreadPort forMode:NSDefaultRunLoopMode];

        do{
            [runloop runMode:NSDefaultRunLoopMode
                  beforeDate:[NSDate distantFuture]];
        }while (YES);
    }
}

从 backgroundThread 向 mainThread 发送信息

- (IBAction)buttonClicked:(id)sender {

    NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:self.mainThreadPort
                                                            receivePort:self.backgroundThreadPort components:nil];
    if (messageObj){
        [messageObj setMsgid:150];
        [messageObj sendBeforeDate:[NSDate date]];
    }
}

这样每次按按钮,就会看到 - (void)handlePortMessage:(NSPortMessage *)message 回调方法被执行了,实现了线程间基于 Port 的通信。注意这个代码时运行在 OS X 上的,因为 iOS 里面 没有 NSPortMessage

参考资料

  1. Run Loops

  2. 深入理解 RunLoop

  3. 走进Run Loop的世界 (一)

  4. 走进Run Loop的世界 (二)

  5. iOS多线程编程指南(三)Run Loop