iOS-RunLoop详解

RunLoop是线程相关的基础设计。

简介

RunLoop是一个事件处理的循环结构,它可以让线程在无须处理事件时进入休眠状态。RunLoop不是完全自动的,需要在程序中显示设计并调用。在Cocoa中,主线程自动集成了RunLoop,而新建的线程没有。

原理

Structure

RunLoop接收两类源的事件,第一类是Input Sources,负责分发异步事件,用于接收从其他线程或者进程发送过来的消息,第二类是Timer Sources,负责分发同步事件,用于处理计时器事件。

下面是RunLoop的结构图。
RunLoopStructure

可以注册run-loop observers,用于接收RunLoop的行为通知。

Mode

RunLoop Mode是由Input Sources和Timers组成的集合。当设置RunLoop的Mode后,只有与该Mode复合的sources才被允许分发事件。

预先定义好的RunLoop Mode。

Mode Name Description
Default NSDefaultRunLoopMode(Cocoa) kCFRunLoopDefaultMode(CoreFoundation) 默认模式
Connection NSConnectionReplyMode(Cocoa) 处理NSConnection相关的事件
Modal NSModalPanelRunLoopMode(Cocoa) 处理Modal Panels相关的事件
Event tracking NSEventTrackingRunLoopMode(Cocoa) 处理鼠标拖动等界面相关事件
Common modes NSDefaultRunLoopMode(Cocoa) kCFRunLoopDefaultMode(CoreFoundation) 一组模式组合,Cocoa包含default、modal、event tracking,CoreFoundation只包含default

对于特定的需求,可以自定义Mode,例如用于过滤一些不需要的事件,或者在对时间有严格要求的操作中,忽略一些低优先级的事件。

自定义Mode方法:

CFRunLoopAddCommonMode

Source

Input Sources

Input Sources,负责分发异步事件.事件的来源取决于source的类型,分为两种:Port-based和Custom。

*Port-Based Sources

由内核自动发出信号,在iOS中不被允许创建。

*Custom Sources

由其他线程发出信号,可以通过CFRunLoopSourceRef方法创建,并通过回调方法进行处理。

下图是一个例子。当主线程有一个任务需要交给Worker线程去处理时,它发送一个命令到command buffer中,完成后,主线程给Input Source发送信号,并唤醒Worker线程的RunLoop。一旦接收到唤醒命令,Worker线程的RunLoop开始处理command buffer中的命令。
Custom Sources

*Cocoa Perform Selector Sources

Cocoa定义了一些Sources可以在任何线程上调用的selector.这些sources在执行完selector后,将自动从runLoop中移除。

注意,只有目标线程定义了runLoop,selector sources才会被处理。

Methods
performSelectorOnMainThread:withObject:waitUntilDone: performSelectorOnMainThread:withObject:waitUntilDone:modes:
performSelector:onThread:withObject:waitUntilDone: performSelector:onThread:withObject:waitUntilDone:modes:
performSelector:withObject:afterDelay: performSelector:withObject:afterDelay:inModes:
cancelPreviousPerformRequestsWithTarget: cancelPreviousPerformRequestsWithTarget:selector:object:

Timer Sources

Timer Sources,负责分发同步事件. 注意Timer fire并不一定会立刻执行,只有runLoop处理该source时,才会开始执行。

Run Loop Observers

Observers将会监听以下几种RunLoop状态。

  • 启动
  • 准备处理timer source
  • 准备处理Input source
  • 准备休眠
  • 唤醒,但还未处理事件
  • 退出

通过CFRunLoopObserverRef创建Observer实例,可以设置是否重复监听。

事件处理的顺序

Run Loop处理事件的顺序如下:

  • (1)通知observers RunLoop启动
  • (2)通知observers timers准备启动
  • (3)通知observers input sources(非port-based)准备启动
  • (4)启动input sources(非port-based)
  • (5)如果有input sources(port-based)已准备好,立刻处理该事件,并跳到(9).
  • (6)通知observers 线程将休眠
  • (7)将线程休眠,直到以下几种事件发生:

    1. 一个事件到达input sources(port-based)
    2. 一个timer启动
    3. runLoop超时
    4. runLoop被显式唤醒
    
  • (8)通知Observers 线程被唤醒

  • (9)处理等待的事件:

    1. 如果一个用户定义的timer启动了,处理该事件,重启runLoop,跳到(2)
    2. 如果一个input source启动了,分发该事件
    3. 如果runLoop被显式唤醒,并还未超时,重启runLoop,跳到(2)
    
  • (10)通知observers RunLoop退出

注意,Observers收到通知和事件真正执行之间存在着时间差,可以通过线程的休眠和唤醒通知来计算该时间差。

一个Run Loop可以通过Run Loop对象显式唤醒,也可以通过事件进行唤醒。

使用

只有手动创建线程时,才可能需要使用RunLoop,需要根据具体使用场景进行分析。对于长时间运行且预先定义好的任务,避免使用RunLoop。

获取RunLoop Object

每个线程有一个单独的RunLoop Object。在Cocoa中,是NSRunLoop,在底层,是CFRunLoopRef的指针。

下面是RunLoop Object的例子。

- (void)threadMain
{
    NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];

    // Create a run loop observer and attach it to the run loop.
    CFRunLoopObserverContext  context = {0, self, NULL, NULL, NULL};
    CFRunLoopObserverRef    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
        kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);

    if (observer)
    {
        CFRunLoopRef    cfLoop = [myRunLoop getCFRunLoop];
        CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
    }

    // Create and schedule the timer.
    [NSTimer scheduledTimerWithTimeInterval:0.1 target:self
            selector:@selector(doFireTimer:) userInfo:nil repeats:YES];

    NSInteger    loopCount = 10;
    do
    {
        // Run the run loop 10 times to let the timer fire.
        [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
        loopCount--;
    }
    while (loopCount);
} 

启动RunLoop Object

RunLoop启动之前,最好设置超时时间以及模式,不然RunLoop一旦启动,将处于无条件状态,终止的唯一方法是kill。
启动RunLoop Object例子如下:

- (void)skeletonThreadMain
{
    BOOL done = NO;

    // Add your sources or timers to the run loop and do any other setup.

    do
    {
        // Start the run loop but return after each source is handled.
        SInt32    result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);

        // If a source explicitly stopped the run loop, or if there are no sources or timers, go ahead and exit.
        if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
            done = YES;

        // Check for any other exit conditions here and set the done variable as needed.
    }
    while (!done);

    // Clean up code here. Be sure to release any allocated autorelease pools.
}

退出RunLoop Object

退出RunLoop Object有三种方法:

  • 设置超时值:RunLoop会完成正常流程,包括发送通知给observers
  • 调用CFRunLoopStop方法:RunLoop会发送仍未发送的通知,并退出
  • 删除RunLoop所有的sources:不太可能的方法,有些系统会自动添加一些source到RunLoop中,而这些source不是由程序创建的,无法删除,导致RunLoop无法退出。

RunLoop Object的线程安全性

Core Foundation中的方法是线程安全的,可以跨线程调用。而NSRunLoop是非线程安全的,只能在单一线程中被调用,否则将crash。