Runloop

注: 本文对照 RunLoop 官方文档翻译, 有不对的地方还请帮忙指正, 谢谢!

目录将就看下吧, 不解释了...

  • Runloop
    • Run Loop Modes
    • Input sources(输入源)
      • Port-Based Sources(基于端口的输入源)
      • Custom Input Sources(自定义输入源)
      • Cocoa Perform Selector Sources(执行选择器源)
    • Timer Sources(定时器源)
    • Run Loop Observers(观察者)
    • The Run Loop Sequence of Events(runloop 的事件队列)
  • When Would You Use a Run Loop?(什么时候使用 runloop)
  • Using Run Loop Objects(使用 runloop 对象)
    • Getting a Run Loop Object(获取一个 runloop 对象)
    • Configuring the Run Loop
    • Starting the Run Loop(启动 runloop)
    • Exiting the Run Loop(退出 runloop)
    • Thread Safety and Run Loop Objects(线程安全和 runloop 对象)
  • Configuring Run Loop Sources(配置 runloop 源)
    • Defining a Custom Input Source(定义一个自定义输入源)
      • Defining the Input Source(定义输入源)
      • Installing the Input Source on the Run Loop(添加源到 runloop)
      • Coordinating with Clients of the Input Source(协调输入源的客户端)
      • Signaling the Input Source(向输入源发送信号)
    • Configuring Timer Sources(配置 timer 源)
    • Configuring a Port-Based Input Source(配置基于 port 的输入源)
      • Configuring an NSMachPort Object(NSMachPort 对象)
      • Configuring an NSMessagePort Object(配置一个 NSMessagePort)
      • Configuring a Port-Based Input Source in Core Foundation(CoreFoundation 中创建 source1)

RunLoop 官方文档.

runloop 是与线程相关的基本基础设施的一部分, 是一个事件处理循环. 可以用它来调度工作并协调传入事件的接收.

  • runloop 有工作时会使当前线程处于忙碌状态, 没有则会让线程休眠;

  • 每个线程都有对应的 runloop, 主线程的 runloop 自动开启, 子线程需要手动开启;

  • runloop 从两种不同类型的源接收事件:

    • 输入源, 传递异步事件, 通常来自另一个线程或另一个程序的消息, 如: port, custom, performSelector: 等;
    • 定时器源 timer, 提供同步事件, 发生在预定时间或重复间隔.
  • 输入源(Input sources)将异步事件交付给相应的处理程序, 会使 runUntilDate: 方法(在线程的关联 NSRunLoop 对象上调用)无效, 即轮播图的问题;

  • 计时器源(Timer sources)将事件交付给它们的处理程序例程,但不会导致运行循环退出.

    RunLoop 与源.png

  • observers, runloop 状态观察者, runloop 会给 run loop observers 发送 runloop 状态的通知, 使观察者在线程上执行额外的处理, 在 CoreFoundation 中可以添加 run loop observers (CFRunLoopAddObserver()).

Run Loop Modes

  • Run Loop Modesrunloop 要监视的输入源和计时器源以及要通知的 observers 的 集合.
  • 每次运行 runloop 时,都需要显示/隐式指定运行模式, 在 runloop 循环过程中, 只监视与该模式相关的源, 并允许交付它们的事件, 并且只有与该模式关联的观察者才会被通知运行循环的进度(状态)。
    释义: runloop 每次循环都有一个 mode, 只允许与该 mode 相关联的源才能把事件给 runloop 处理, runloop 的循环进度也只有与该 mode 相关联的观察者才会被通知.
  • 可以指定 runloop 运行 mode, 但必须添加一个或多个输入源, timer 或者观察者到这个 mode 中, 才能使该 mode 起效.
  • runloop 使用 mode 过滤来自不需要的源的事件, 大多数情况下你希望以系统定义的 default mode 下运行, 但是模态面板下可能以 modal mode 运行, 在这种 mode 下, runloop 只处理 modal mode 下的源的事件. 对于辅助线程, 可以使用自定义 mode 防止低优先级的源在时间关键的操作期间交付事件(当前是 A mode, B mode 的事件放到自定义 mode 中处理, 因为当前 A 的源的事件是优先级最高的).

注意 : 模式的区分基于事件的源, 而不是事件的类型, 比如, 你不能使用 mode 来匹配鼠标向下事件或键盘事件, 但可以使用 mode 监听一组不同的端口, 临时挂起计时器, 或者改变源触发当前被监视的 runloop observers.

释义: mode 只能用来匹配源, 不能匹配事件, 也就是 mode 包含的是各种源, 源包含各种事件的这种对应关系.

Predefined run loop modes :

Model Name Description
Default NSDefaultRunloopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) 默认模式用于大多数操作。大多数情况下,您应该使用此模式启动运行循环并配置输入源.
Connection NSConnectionReplyMode (Cocoa) Cocoa将此模式与NSConnection对象结合使用来监视响应.
Modal NSModalPanelRunLoopMode (Cocoa) Cocoa使用此模式来标识用于模态面板的事件.
Event tracking NSEventTrackingRunLoopMode (Cocoa) Cocoa使用此模式来限制鼠标拖动循环和其他用户界面跟踪循环期间的传入事件.
Common modes NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation) 这是一组可配置的常用模式。将输入源与此模式关联也将其与组中的每个模式关联。对于Cocoa应用程序,默认情况下,这个集合包括默认模式、模式和事件跟踪模式。Core Foundation最初只包含默认模式, 可以使用CFRunLoopAddCommonMode函数向集合添加自定义模式.

Input sources(输入源)

输入源将事件 异步 地交付给线程, 事件的源取决于输入源的类型, 通常分为两类:

  • Port-Based Sources(基于端口的源)监视应用程序的 mach port
  • Custom Input Sources(自定义输入源)监视事件的自定义源

系统实现了具有代表性的两种类型的源, 两个源之间的唯一区别就是它们如何发出信号的, 基于端口的源是通过内核自动发出信号, 而自定义的源则必须从另一个线程手动发出信号.
创建输入源时, 会把输入源分配到 runloop 的一个或多个 mode 下, 任何时候添加的源都会被 mode 监视. 大多数情况下, 运行默认的 default mode, 也可以指定自定义源, 如果输入源不是在当前 mode 下被监视, 那么源生成的任何事件都将等待, 直到 runloop 在对应的 mode 下才会被执行.

Port-Based Sources(基于端口的输入源)

CocoaCoreFoundation 为使用 Port-related(端口相关) 对象和函数创建 Port-Based 输入源提供内联支持, 例如, 在 Cocoa 中, 根本不需要直接创建输入源, 只需要创建一个端口对象并且使用 NSPort 类的方法将 port 添加到 runloop, port 对象处理输入源需要的创建和配置.
CoreFoundation 中, 必须手动创建端口及其运行循环源.
在这两种情况下, 都是通过使用与端口不透明类型(CFMachPortRefCFMessagePortRefCFSocketRef)相关的函数创建对象的.

Custom Input Sources(自定义输入源)

CoreFoundation 中必须使用与不透明类型 CFRunLoopSourceRef 相关的函数创建一个自定义输入源, 使用几个回调函数配置自定义输入源, CoreFoundation 在配置源, 处理事件以及在从 runloop 中销毁源时会调用配置的回调函数.
除了事件到达时的自定义源的行为外, 还需要定义事件交付机制, 源的这一部分运行在一个单独的线程上, 负责向输入源提供数据, 并且在数据准备处理时发出信号. 事件交付机制由创建者决定.

Cocoa Perform Selector Sources(执行选择器源)

除了基于端口的源之外, Cocoa 还定义了一个可以在任何线程上 perform selector 的自定义输入源, 和基于端口的源类似, perform selector 的请求在目标线程上序列化, 从而缓解了在一个线程上运行多个方法时可能出现的许多同步问题; 与基于端口的源不同的是 perform selector 源会在执行完成之后将自身从 runloop 中移除.

当在另一个线程 performming a selector 时, 目标线程必须有一个活跃的 runloop, 对于创建的线程(非主线程), 这意味着需要等到代码显式的开启 runloop, 因为主线程会启动自身的 runloop, 所以只要应用程序调用了 applicationDidFinishLaunching: 代理方法, 就可以对主线程发起调用, runloop 一次循环处理所有在等待的 Perform Selector, 而不是每次只调用一个.

NSObject 上定义的方法, 用于在其他线程上执行选择器, 这些方法实际上并不创建执行选择器的新线程

  • Performing selectors on other threads
Methods Description
performSelectorOnMainThread:withObject:waitUntilDone: performSelectorOnMainThread:withObject:waitUntilDone:modes: 在主线程的下次 runloop 循环中执行特定的 selector, 提供在执行 selector 之前阻塞当前线程的选项.
performSelector:onThread:withObject:waitUntilDone: performSelector:onThread:withObject:waitUntilDone:modes: 在具有NSThread对象的任何线程上执行指定的选择器, 提供在执行 selector 之前阻塞当前线程的选项.
performSelector:withObject:afterDelay: performSelector:withObject:afterDelay:inModes: 在当前线程的下次循环中延迟一定时间后执行特定的 selector, 多个队列选择器按它们排队的顺序依次执行
cancelPreviousPerformRequestsWithTarget: cancelPreviousPerformRequestsWithTarget:selector:object: 取消使用延迟(afterDelay:)发送到当前线程的消息。

Timer Sources(定时器源)

定时器源在将来的某个预设时间将事件同步地交付给线程, 定时器是线程通知自身做某事的一种方式.

尽管定时器生成基于时间的通知,但它并不是一种实时机制。与输入源一样,计时器与 runloop mode 相关联。如果计时器没有处于runloop 当前监视的模式,则在以 计时器支持的模式之一 运行 runloop 之前,它不会触发。类似地,如果计时器在 runloop 执行处理程序例程时触发,则计时器将等到下一次执行运行循环时调用其处理程序例程。如果 runloop 根本不运行,那么定时器就不会触发。
定时器可以生成一次或重复事件, 重复事件根据预定触发时间(不是实际触发时间)重新调度自身. 举例(轮播图与 scrollView 的问题).

Run Loop Observers(观察者)

与在适当的异步或同步事件发生时的触发源相反,运行循环观察者在 runloop 本身执行期间在特定位置触发源. 可以使用 run loop observer 来准备线程来处理给定的事件,或者在线程休眠之前准备线程.
runloop 中主要有以下事件可以触发 run loop observer:

  • The entrance to the run loop. -- runloop 进入循环时
  • When the run loop is about to process a timer. -- runloop 将要处理计时器时
  • When the run loop is about to process an input source. -- runloop 将要处理输入源时
  • When the run loop is about to go to sleep. -- runloop 将要进入睡眠时
  • When the run loop has woken up, but before it has processed the event that woke it up. -- runloop 被事件唤醒, 还未处理该事件时
  • The exit from the run loop. -- runloop 退出循环时

对应可选类型:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

可以使用 CoreFoundation 向应用程序添加运行循环观察者, 要创建一个 runloop observer, 需要创建一个 CFRunLoopObserverRef 实例, 该类型跟踪自定义回调函数和想要监听的活动(runloop 的状态).
runloop observers 也可以设置一次或多次监听, 一次性的在执行之后会从 runloop 中删除自身, 重复的 observer 会保持附加.

The Run Loop Sequence of Events(runloop 的事件队列)

每次运行时,线程的运行循环处理等待的事件并为任何附加的观察者生成通知, 具体顺序如下:

  1. Notify observers that the run loop has been entered. -- 通知观察者 runloop 已经进入循环
  2. Notify observers that any ready timers are about to fire. -- 通知观察者 timers 将要触发事件
  3. Notify observers that any input sources that are not port based are about to fire. -- 通知观察者 不是基于端口的输入源 将要触发
  4. Fire any non-port-based input sources that are ready to fire. -- 通知观察者 不是基于端口的输入源 触发了
  5. If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9. -- 如果一个基于端口的输入源准备且等待触发, 立即处理该事件 ==> step 9.
  6. Notify observers that the thread is about to sleep. -- 通知观察者 线程将要休眠
  7. Put the thread to sleep until one of the following events occurs: -- 将线程线程置为休眠直到下面情况出现
    • An event arrives for a port-based input source. -- 基于端口的输入源的事件到达
    • A timer fires. -- timer 事件触发
    • The timeout value set for the run loop expires. -- 超过了为运行循环设置的超时值
    • The run loop is explicitly woken up. -- 被显式地唤醒
  8. Notify observers that the thread just woke up. -- 通知观察者 线程刚刚被唤醒
  9. Process the pending event. -- 处理等待的事件
    • If a user-defined timer fired, process the timer event and restart the loop. Go to step 2. -- 如果一个用户定义的 timer 触发, 处理 timer 事件, 重新启动循环 ==> step 2.
    • If an input source fired, deliver the event. -- 如果输入源触发, 交付事件
    • If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2. -- 如果 runloop 被显示的唤醒, 但还未超时, 重新启动循环 ==> step 2.
  10. Notify observers that the run loop has exited. -- 通知观察者 runloop 已经退出.

对应流程如下图:

RunLoop 执行流程.png

由于计时器和输入源的观察者通知是在这些事件实际发生之前交付的,因此通知的时间和实际事件的时间之间可能存在差距。如果这些事件之间的时间非常关键,则可以使用 sleepsleep-from-sleep通知来帮助您关联实际事件之间的时间。
可以使用 runloop 对象显示地唤醒 runloop,其他事件也可能使 runloop 被唤醒, 例如, 添加另一个非基于端口的输入源将唤醒运行循环,以便可以立即处理输入源,而不是等到其他事件发生. (timer)

When Would You Use a Run Loop?(什么时候使用 runloop)

唯一需要显示地 run the runloop 是为应用程序创建辅助线程(子线程)的时候, 应用主线程的 runloop 是至关重要的基础设施, 因此,应用程序框架提供了运行主应用程序循环并自动启动该循环的代码。iOS 中 UIApplication(OS X 中的 NSApplication) 的 run 方法启动应用程序的主循环作为正常启动序列中的一部分.
子线程中,你需要决定是否需要一个 runloop, 如果是, 你自己配置并启动它.

在所有情况下都不需要启动线程的运行循环. 例如, 如果使用一个线程 perform 一些长时间运行且预先确定的 task, 可以避免启动 runloop. 适用于需要与线程进行更多交互的情况, 例如, 如果想要执行以下任何操作, 都需要开启一个 runloop:

  • Use ports or custom input sources to communicate with other threads.
    -- 使用基于接口或者自定义输入源来与其他线程进行通讯.
  • Use timers on the thread.
    -- 在线程上使用 timers
  • Use any of the performSelector… methods in a Cocoa application.
    -- 在 Cocoa 应用中使用任何 performSelector... 方法
  • Keep the thread around to perform periodic tasks.
    -- 保留线程以执行周期性任务

如果决定使用 runloop, 那么配置是非常简单的. 但是, 和所有的线程编程一样, 你应该有计划的使用, 以便在适当的情况下退出子线程.
通过让线程退出干净地结束 runloop 总比强制终止的好.

Using Run Loop Objects(使用 runloop 对象)

runloop 对象提供了主入接口, 用于向 runloop 中添加 input sources, timers, and run-loop observers 并且运行它.
每个线程都有一个 runloop 对象与之对应, 在 Cocoa 中, 这个对象是 NSRunLoop 类的实例. 在底层应用程序中, 是一个指向 CFRunLoopRef 类型的指针.

Getting a Run Loop Object(获取一个 runloop 对象)

获取给当前线程的 runloop :

  • In a Cocoa application, use the currentRunLoop class method of NSRunLoop to retrieve an NSRunLoop object. -- 在 Cocoa 应用中, 使用 NSRunLoop 中的 currentRunLoop 类方法获取.
  • Use the CFRunLoopGetCurrent function. -- 使用 CFRunLoopGetCurrent 函数.

可以从 NSRunLoop 对象获得 CFRunLoopRef 不透明类型. NSRunLoop 类定义了一个 getCFRunLoop 方法,该方法返回一个可以传递给 CoreFoundation 例程的 CFRunLoopRef 类型. 因为这两个对象引用同一个 runloop,所以可以根据需要混合调用 NSRunLoop 对象和 CFRunLoopRef 不透明类型.

Configuring the Run Loop

在 run the runloop 之前, 必须给 runloop 添加至少一个输入源或者 timer, 如果 runloop 在没有任何源需要监视, 在你尝试 run 的时候 runloop 会立即退出.
除了添加源之外, 还可以添加 runloop observer, 用他们来监视 runloop 的不同执行阶段. 想要添加 runloop observer, 需要创建一个 CFRunLoopObserverRef 类型, 使用 CFRunLoopAddObserver 函数来把它添加到 runloop 中, 必须使用 CoreFoundation 创建 run loop observer, 即使对于 Cocoa 应用程序也是如此。

官方示例(主线程):

- (void)threadMain {
    // The application uses garbage collection, 
    // so no autorelease pool is needed.
    // 获取当前线程
    NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
 
    // Create a run loop observer and attach it to the run loop.
    // 创建一个 runloop observer 附加到 runloop 中.
    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.
    // 创建并执行定时器, 该方法会直接创建一个 timer 加到当前的\
    // runloop 中以默认的方式执行.
    [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,但一次性定时器通常触发后通常会失效, 这会导致 runloop 退出, 附加一个重复性的计时器可以使 runloop 运行更长时间, 但这需要周期性的触发定时器以唤醒 runloop, 这实际上是轮询的另一种形式, 相反, 输入源等待时间发生保证线程休眠直到事件发生.

Starting the Run Loop(启动 runloop)

在应用程序中, 只有子线程需要启动 runloop, runloop 至少监视一个输入源或者 timer, 如果没有一个附加源, runloop 会立即退出.
开启 runloop 的几种方式:

  • Unconditionally -- 没有条件的
    • - (void)run;
  • With a set time limit -- 设置时间限制
    • - (void)runUntilDate:(NSDate *)limitDate;
  • In a particular mode -- 特定的 mode
    • - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;

unconditionally :

NSDefaultRunLoopMode 中运行反复调用 runMode:beforeDate:, 相当于开启了一个无限循环.

最简单的方式, 但是也是最不可取的.
会使 runloop 进入永久循环, 这使得你对通过 runloop 自身的控制非常少.

可以添加和删除输入源和定时器, 但是停止 runloop 的唯一方法就是杀死它.
没有办法通过自定义 mode 启动 runloop.

With a set time limit :
通过反复调用 runMode:beforeDate: 直到指定的过期日期,在 NSDefaultRunLoopMode 中运行接收器。

unconditionally 更好的启动 runloop 的方法是使用超时值启动 runloop, 当使用超时值时, runloop 将会一直运行直到事件到达或者超过超时值. 如果事件到达, runloop 会在处理完事件后人后退出 runloop, 然后可以重新启动 runloop 处理下一个事件; 如果是超时, 则只需要重新启动 runloop 或者在这个时刻执行必要的清理工作.

In a particular mode :

使用特定的模式运行运行循环
模式限制了向运行循环交付事件的源的类型, 从运行循环中手动删除所有已知的输入源和计时器并不保证运行循环将立即退出。macOS可以根据需要安装和删除额外的输入源,以处理针对接收方线程的请求。因此,这些源可以防止run循环退出。

Schedules the execution of a block :

  • - (void)performInModes:(NSArray *)modes block:(void (^)(void))block
    • Schedules the execution of a block on the target run loop in given modes.
      -- 在 runloop 指定 mode 中执行 block
  • - (void)performBlock:(void (^)(void))block;
    • Schedules the execution of a block on the target run loop.
      -- 在 runloop 中执行 block

running a runloop(官方示例):

- (void)skeletonThreadMain {
    // Set up an autorelease pool here if not using garbage collection.
    BOOL done = NO;
 
    // Add your sources or timers to the run loop and do any other setup.
    // 添加源或者 timer 进 runloop 以及其他一些设置
    do {
        // Start the run loop but return after each source is handled.
        // 启动 runloop, 但在处理每个源之后返回.
        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.
        // 如果一个源显示的 stop runloop 或者没有源或 timers, runloop 退出.
        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, 也就是说可以调用 CFRunLoopRunCFRunLoopRunInMode 或任何 NSRunLoop 方法来从输入源或计时器的处理程序例程中启动运行循环.

Exiting the Run Loop(退出 runloop)

有两种方式可以让 runloop 在处理事件之前退出:

  • Configure the run loop to run with a timeout value. -- 配置 timeout
  • Tell the run loop to stop. -- 告诉 runloop stop.

如果是管理 runloop, 则使用超时值当然是首选. 指定超时值可以让运行循环在退出之前完成所有的正常处理, 包括向 runloop observer 发送通知.

使用 CFRunLoopStop 函数显式地停止 runloop 会产生类似超时的结果. runloop 在发送所有剩余的 runloop 状态通知后退出. 不同之处在于, 可以在无条件启动的 runloop 中使用.

尽管删除 runloop 的源和定时器可能也会使 runloop 退出, 但是这个方法并不可靠, 因为一些系统例程将输入源添加到 runloop 中以处理所需的事件, 删除的时候可能不知道这些源, 所以会阻止 runloop 的退出.

Thread Safety and Run Loop Objects(线程安全和 runloop 对象)

线程安全性取决于使用哪个API来操作 runloop. CoreFoundation 中的函数通常是线程安全的, 可以从任何线程调用. 但是, 如果您正在执行 更改 runloop 配置的操作, 最好还是尽可能从拥有 runloop 的线程开始执行.
NSRunLoop 类本身并不像它的核心基础类那样线程安全, 如果使用 NSRunLoop 类来修改您的 runloop, 应该只从拥有该 runloop 的同一个线程进行修改, 将输入源或计时器添加到属于不同线程runloop 中可能会导致代码崩溃或以意想不到的方式运行.

Configuring Run Loop Sources(配置 runloop 源)

Defining a Custom Input Source(定义一个自定义输入源)

定义一个自定义输入源需要使用 CoreFoundation 配置源并且添加到 runloop 中.
创建自定义输入源需要如下定义:

  • The information you want your input source to process.
    • CFRunLoopSourceRef runLoopSource;
    • NSMutableArray* commands;
  • A scheduler routine to let interested clients know how to contact your input source.
    • void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
  • A handler routine to perform requests sent by any clients.
    • void RunLoopSourcePerformRoutine (void *info);
  • A cancellation routine to invalidate your input source.
    • void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);

自定义源是为了处理自定义信息, 实际配置也是灵活的, 大多数自定义输入源总是需要 RunLoopSourceScheduleRoutine / RunLoopSourcePerformRoutine / RunLoopSourceCancelRoutine 三个关键的回调函数. 你可以定义一种机制将数据传递到自定义的输入源, 并用这个源与其他线程通信.

自定义输入源的示例配置(官方示例):
应用程序的主线程维护对输入源的引用、该输入源已拥有 自定义命令缓冲区 以及 runloop.

当主线程有一个任务要传递给工作线程时, 它会向命令缓冲区发送一个命令以及工作线程启动任务所需的任何信息. (因为主线程和工作线程的输入源都可以访问命令缓冲区, 所以必须是同步访问), 一旦发出命令, 主线程主线程向工作线程发出信号, 并唤醒工作线程的 runloop, 接收到唤醒命令后, runloop 会根据在命令缓冲区中找到的命令调用输入源的回调函数.

Operating a custom input source

自定义源.png

Defining the Input Source(定义输入源)

上图显示输入源使用 Objective-C 对象来管理命令缓冲区并与 runloop 进行协调。

下面是对这个对象的定义. RunLoopSource 对象管理一个命令缓冲区,并使用该缓冲区接收来自其他线程的消息. RunLoopContext 实际上只是一个容器对象, 用于将 RunLoopSource 对象和 runloop 引用传递给应用程序的主线程。

The custom input source object definition(自定义输入源对象定义):

@interface RunLoopSource : NSObject {
    CFRunLoopSourceRef runLoopSource;
    NSMutableArray* commands;
}
 
- (id)init;
- (void)addToCurrentRunLoop;
- (void)invalidate;
 
// Handler method
- (void)sourceFired;
 
// Client interface for registering commands to process
// 用于注册要处理的命令的客户端接口
- (void)addCommand:(NSInteger)command withData:(id)data;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;
 
@end
 
// These are the CFRunLoopSourceRef callback functions.
void RunLoopSourceScheduleRoutine (void *info, 
                           CFRunLoopRef rl, 
                            CFStringRef mode);
void RunLoopSourcePerformRoutine (void *info);
void RunLoopSourceCancelRoutine (void *info, 
                         CFRunLoopRef rl, 
                          CFStringRef mode);
 
// RunLoopContext is a container object used during\
// registration of the input source.
// RunLoopContext 是在注册输入源时使用的容器对象
@interface RunLoopContext : NSObject {
    CFRunLoopRef        runLoop;
    RunLoopSource*        source;
}
@property (readonly) CFRunLoopRef runLoop;
@property (readonly) RunLoopSource* source;
 
- (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop;
@end

RunLoopSourceScheduleRoutine

虽然 OC代码管理 source 的自定义数据, 但是回调函数都是基于 C 的, RunLoopSourceScheduleRoutine 函数是在 source 添加到 runloop 中时回调, 比如如下代码: 因为这个输入源只有一个客户机(主线程), 所以它通过 RunLoopSourceScheduleRoutine 函数发送一条消息, 将自己注册到该线程上的应用程序代理, 当代理希望与 source 通信时, 就会使用 RunLoopContext 对象中的信息进行通信.

Scheduling a run loop source

void RunLoopSourceScheduleRoutine (void *info, 
                           CFRunLoopRef rl, 
                            CFStringRef mode) {
    // 获取 RunLoopSource 对象
    RunLoopSource* obj = (RunLoopSource*)info;
    // 设置应用程序代理
    AppDelegate*   del = [AppDelegate sharedAppDelegate];
    // 获取 RunLoopContext 对象
    RunLoopContext* theContext = [[RunLoopContext alloc]
                                          initWithSource:obj andLoop:rl];
 
    // 代理通过 congtext 在主线程中执行注册方法.
    [del performSelectorOnMainThread:@selector(registerSource:)
                                withObject:theContext waitUntilDone:NO];
}

RunLoopSourcePerformRoutine

输入源发出一个信号用来处理自定义数据的回调, 如下代码显示了与 RunLoopSource 相关联的 RunLoopSourcePerformRoutine 调用, 这个函数只是将开始工作的请求转发给了 sourceFired 方法, 然后该方法会处理在命令缓冲区中的命令.

Performing work in the input source

void RunLoopSourcePerformRoutine (void *info) {
    RunLoopSource*  obj = (RunLoopSource*)info;
    // 调用 sourceFired
    [obj sourceFired];
}

RunLoopSourceCancelRoutine

如果使用 CFRunLoopSourceInvalidate 函数将输入源从 runloop 中移除, 系统将调用 RunLoopSourceCancelRoutine 函数, 可以在这个函数中通知客户端该输入源已经失效以让客户端移除对该源的引用, 下面的代码显示了 RunLoopSourceRunLoopSourceCancelRoutine 回调处理, 该函数给应用程序代理发送了又一个 RunLoopContext 对象, 但是这次是让应用程序代理移除对 source 的引用.

Invalidating an input source

void RunLoopSourceCancelRoutine (void *info, 
                         CFRunLoopRef rl, 
                          CFStringRef mode) {
    RunLoopSource* obj = (RunLoopSource*)info;
    AppDelegate* del = [AppDelegate sharedAppDelegate];
    // 获取源对应的 context
    RunLoopContext* theContext = [[RunLoopContext alloc] 
                                          initWithSource:obj andLoop:rl];
 
    // 让代理移除 source 的引用
    [del performSelectorOnMainThread:@selector(removeSource:)
                                withObject:theContext waitUntilDone:YES];
}

registerSource: and removeSource: 方法在下面 Coordinating with Clients of the Input Source.

Installing the Input Source on the Run Loop(添加源到 runloop)

如下代码显示了 RunLoopSourceinit 方法和 addToCurrentRunLoop 方法, init 方法创建的 CFRunLoopSourceRef 对象, 必须附加到 runloop 中, RunLoopSource 会将自身作为上下文信息传递以至于回调函数可以用指针指向它, 直到工作线程调用 addToCurrentRunLoop 方法才会添加源到 runloop, 同时会执行 RunLoopSourceScheduleRoutine 回调, 一旦添加完成, 线程就会 run the runloop 等待 source 触发事件.

Installing the run loop source

- (id)init {
    // 创建一个 sourceContext
    CFRunLoopSourceContext    context = {0, self, 
                                        NULL, NULL, NULL, NULL, NULL,
                                        &RunLoopSourceScheduleRoutine,
                                        RunLoopSourceCancelRoutine,
                                        RunLoopSourcePerformRoutine};
    // 创建 source
    runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
    commands = [[NSMutableArray alloc] init];
 
    return self;
}
 
- (void)addToCurrentRunLoop {
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    // 添加 source 到 runloop
    CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
}

Coordinating with Clients of the Input Source(协调输入源的客户端)

想要创建的输入源有用, 那么你需要操作它并从另一个线程发出信号. 输入源的意义在于让与之相关的线程休眠直到源触发事件, 这样就需要让其他线程知道并有方法去和它进行通信.

通知客户端的一个方法就是在首次将 source 添加到 runloop 中时, 发出注册请求, 可以注册任意想要注册的客户端, 或者可以向中央代理注册, 然后给到想要注册的客户端.

下面的代码显示了应用程序代理注册方法, 并在 RunLoopSource 对象执行 scheduler 方法被调用的时候执行. 该方法接收由RunLoopSource 对象提供的 RunLoopContext 对象并添加到源列表中; 也包括从 runloop 中删除源时的注销源的回调方法.

Registering and removing an input source with the application delegate(使用应用程序代理注册和删除输入源)

- (void)registerSource:(RunLoopContext*)sourceInfo {
    // 注册 context
    [sourcesToPing addObject:sourceInfo];
}
 
- (void)removeSource:(RunLoopContext*)sourceInfo {
    id    objToRemove = nil;

    // 遍历找出对应的 context 
    for (RunLoopContext* context in sourcesToPing) {
        if ([context isEqual:sourceInfo]) {
            objToRemove = context;
            break;
        }
    }
 
    // 删除要删除的 context
    if (objToRemove)
        [sourcesToPing removeObject:objToRemove];
}

Signaling the Input Source(向输入源发送信号)

在把数据传递给输入源后(即自定义输入源完成后), 客户端必须向输入源发送信号并唤醒它的 runloop. 发送信号让 runloop 知道输入源准备好被处理. 由于发送信号的时候线程可能处于休眠状态, 所以应该显示的唤醒 runloop, 如果不唤醒 runloop 的话, 会导致源事件处理被延时.
客户端准备好处理他们加到缓冲区里的命令时, 会调用 RunLoopSourcefireCommandsOnRunLoop 方法:

Waking up the run loop(唤醒 runloop)

- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop {
    CFRunLoopSourceSignal(runLoopSource);
    CFRunLoopWakeUp(runloop);
}

Configuring Timer Sources(配置 timer 源)

想要创建一个 timer 源, 必须创建一个 timer 对象并且把它加到 runloop 中, Cocoa 中的 NSTimer, CoreFoundation 中的 CFRunLoopTimerRef. NSTimer 内部实现其实就是对 CoreFoundation 的扩展, 提供了便利特性, 类似创建和添加 timer 使用同一个方法:

  • scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
  • scheduledTimerWithTimeInterval:invocation:repeats:

这两个方法创建 timer 并将其以 NSDefaultRunLoopMode 默认模式添加到当前线程的 runloop 中, 也可以通过 NSRunLoopaddTimer:forMode: 方法创建一个 NSTimer 对象并手动将其以不同 mode下添加到 runloop 中.

Creating and scheduling timers using NSTimer(Cocoa 中创建)

NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
 
// Create and schedule the first timer.
NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate
                        interval:0.1
                        target:self
                        selector:@selector(myDoFireTimer1:)
                        userInfo:nil
                        repeats:YES];
[myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode];
 
// Create and schedule the second timer.
[NSTimer scheduledTimerWithTimeInterval:0.2
                        target:self
                        selector:@selector(myDoFireTimer2:)
                        userInfo:nil
                        repeats:YES];

Creating and scheduling a timer using Core Foundation(CoreFoundation 中创建)

CFRunLoopRef runLoop = CFRunLoopGetCurrent();
// use this structure to pass around any custom data\
// you needed for your timer.
// 可以根据需要为 timer 传递任何环境变量.
CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};
/*
 CFRunLoopTimerRef CFRunLoopTimerCreate(CFAllocatorRef allocator, 
                                        CFAbsoluteTime fireDate, 
                                        CFTimeInterval interval, 
                                         CFOptionFlags flags, 
                                               CFIndex order, 
                                CFRunLoopTimerCallBack callout,  
                                 CFRunLoopTimerContext *context);
*/
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 
                                                               0.1, 
                                                               0.3, 
                                                                 0, 
                                                                 0,
                                                &myCFTimerCallback, 
                                                          &context);
 
CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);

Configuring a Port-Based Input Source(配置基于 port 的输入源)

CocoaCoreFoundation 都提供了基于端口的对象,用于线程或进程之间的通信.

Configuring an NSMachPort Object(NSMachPort 对象)

要建立与本地连接的 NSMachPort 对象, 需要创建一个 port 对象并将它添加到主线程的 runloop 中, 当启动子线程时, 传递相同对象(port)给子线程的入口函数. 子线程将使用这个对象发送消息返回给主线程.
主线程启动子线程处理事件:

Implementing the Main Thread Code

Main thread launch method

- (void)launchThread {
// 创建 port
    NSPort* myPort = [NSMachPort port];
    if (myPort) {
        // This class handles incoming port messages.
        [myPort setDelegate:self];
 
        // Install the port as an input source on the current run loop.
        // 添加到 runloop
        [[NSRunLoop currentRunLoop] addPort:myPort 
                                    forMode:NSDefaultRunLoopMode];
 
        // Detach the thread. Let the worker release the port.
        // 将 port 传递给子线程, 由子线程进行释放
        [NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
               toTarget:[MyWorkerClass class] withObject:myPort];
    }
}

为了在线程之间设置双向通信, 可以让工作线程在消息中向主线程发送自己的本地端口, 接收到消息让主线程知道启动的子线程一切顺利, 也提供了向该线程发送进一步消息的方法。
主线程的 handlePortMessage: 方法如下, 该方法在数据到达线程的本地端口(port)时调用. 当消息到达时, 该方法直接从端口检索子线程的端口, 并将其保存起来供以后使用 :

#define kCheckinMessage 100  // 端口号
 
// Handle responses from the worker thread.
- (void)handlePortMessage:(NSPortMessage *)portMessage {
    unsigned int message = [portMessage msgid];
    NSPort * distantPort = nil;
 
    if (message == kCheckinMessage) {
        // Get the worker thread’s communications port.
        distantPort = [portMessage sendPort];
 
        // Retain and save the worker port for later use.
        [self storeDistantPort:distantPort];
    } else {
        // Handle other messages.
    }
}

Implementing the Secondary Thread Code

子线程必须配置并且需要指定 用来和将通信信息返回给主线程的 端口.
配置子线程, 代码如下, 在为线程创建一个自动释放池之后, 然后创建了一个 worker 对象以驱动线程执行, workersendCheckinMessage: 方法为工作线程创建了一个本地 port 并发送消息返回给主线程.

Launching the worker thread using Mach ports

+ (void)LaunchThreadWithPort:(id)inData {
    NSAutoreleasePool*  pool = [[NSAutoreleasePool alloc] init];
 
    // Set up the connection between this thread and the main thread.
    // 设置当前线程和主线程之间的连接
    NSPort* distantPort = (NSPort*)inData;

    MyWorkerClass*  workerObj = [[self alloc] init];
    // 这里传入当前线程的 port, 会被 workerObj 对象作为远程 port. \
    // workerObj 自身也会创建一个自己的本地 port 用做通信.
    [workerObj sendCheckinMessage:distantPort];
    [distantPort release];
 
    // Let the run loop process things.
    do {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:[NSDate distantFuture]];
    }
    while (![workerObj shouldExit]);
 
    [workerObj release];
    [pool release];
}

// Worker thread check-in method
- (void)sendCheckinMessage:(NSPort*)outPort {
    // Retain and save the remote port for future use.
    // 引用并保存远程端口以供将来使用
    [self setRemotePort:outPort];
 
    // Create and configure the worker thread port.
    // 创建并配置工作线程端口
    NSPort* myPort = [NSMachPort port];
    [myPort setDelegate:self];
    [[NSRunLoop currentRunLoop] addPort:myPort 
                                forMode:NSDefaultRunLoopMode];
 
    // Create the check-in message.
    // 创建消息
    NSPortMessage* messageObj = [[NSPortMessage alloc] 
                                        initWithSendPort:outPort
                                             receivePort:myPort 
                                              components:nil];
 
    if (messageObj) {
        // Finish configuring the message and send it immediately.
        // 完成消息配置并立即发送
        [messageObj setMsgId:setMsgid:kCheckinMessage];
        [messageObj sendBeforeDate:[NSDate date]];
    }
}

当使用 NSMachPort 时, 本地线程和远程线程可以使用同一个端口对象进行线程之间的单向通信. 也就是说, 一个线程创建的本地端口对象会变为另一个线程的远程端口对象.

Configuring an NSMessagePort Object(配置一个 NSMessagePort)

使用 NSMessagePort 对象建立本地连接, 不能简单的在线程间传递 port 对象. 必须通过 name 获取, 在 Cocoa 中需要用一个特殊的 name 注册本地端口, 然后传递给远程线程以便它能够获得适当的 port 对象进行通信.

Registering a message port

NSPort* localPort = [[NSMessagePort alloc] init];
 
// Configure the object and add it to the current run loop.
[localPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:localPort 
                            forMode:NSDefaultRunLoopMode];
 
// Register the port using a specific name. The name must be unique.
NSString* localPortName = [NSString stringWithFormat:@"MyPortName"];
[[NSMessagePortNameServer sharedInstance] registerPort:localPort
                                                  name:localPortName];

Configuring a Port-Based Input Source in Core Foundation(CoreFoundation 中创建 source1)

下面的代码显示了主线程启动工作线程, 首先设置了一个 CFMessagePortRef 对象监听来自工作线程的消息. 工作线程需要 portname 来建立连接, 以便将字符串值传递给工作线程的入口函数, name 通常唯一, 否则会发生冲突.

Attaching a Core Foundation message port to a new thread(添加 CoreFoundationmessage port 到一个新的线程):

#define kThreadStackSize        (8 *4096)
 
OSStatus MySpawnThread() {
    // Create a local port for receiving responses.
    CFStringRef myPortName;
    CFMessagePortRef myPort;
    CFRunLoopSourceRef rlSource;
    CFMessagePortContext context = {0, NULL, NULL, NULL, NULL};
    Boolean shouldFreeInfo;
 
    // Create a string with the port name.
    myPortName = CFStringCreateWithFormat(NULL, 
                                          NULL,
                  CFSTR("com.myapp.MainThread"));
 
    // Create the port.
    myPort = CFMessagePortCreateLocal(NULL,
                                myPortName,
                &MainThreadResponseHandler,
                                  &context,
                           &shouldFreeInfo);
 
    if (myPort != NULL) {
        // The port was successfully created.
        // Now create a run loop source for it.
        rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
 
        if (rlSource) {
            // Add the source to the current run loop.
            CFRunLoopAddSource(CFRunLoopGetCurrent(), 
                                            rlSource, 
                               kCFRunLoopDefaultMode);
 
            // Once installed, these can be freed.
            // 添加完成, 释放变量.
            CFRelease(myPort);
            CFRelease(rlSource);
        }
    }
 
    // Create the thread and continue processing.
    // 创建线程并继续处理
    MPTaskID        taskID;
    return(MPCreateTask(&ServerThreadEntryPoint,
                              (void*)myPortName,
                               kThreadStackSize,
                                           NULL,
                                           NULL,
                                           NULL,
                                              0,
                                        &taskID));
}

MainThreadResponseHandler
主线程回调处理函数

#define kCheckinMessage 100
 
// Main thread port message handler
CFDataRef MainThreadResponseHandler(CFMessagePortRef local,
                                              SInt32 msgid,
                                           CFDataRef data,
                                               void* info) {
    if (msgid == kCheckinMessage) {
        CFMessagePortRef messagePort;
        CFStringRef threadPortName;
        CFIndex bufferLength = CFDataGetLength(data);
        UInt8 * buffer = CFAllocatorAllocate(NULL, bufferLength, 0);
 
        // 获取 data 信息, 解析 port name
        CFDataGetBytes(data, CFRangeMake(0, bufferLength), buffer);
        threadPortName = CFStringCreateWithBytes (NULL, 
                                                buffer, 
                                          bufferLength, 
                                kCFStringEncodingASCII, 
                                                 FALSE);
 
        // You must obtain a remote message port by name.
        // 通过 name 获得远程 message port
        messagePort = CFMessagePortCreateRemote(NULL, 
                         (CFStringRef)threadPortName);
 
        if (messagePort) {
            // Retain and save the thread’s comm port for future reference
            // 保存 message port
            AddPortToListOfActiveThreads(messagePort);
 
            // Since the port is retained by the previous function, \
            // release it here.
            // 被上面的方法保存后释放.
            CFRelease(messagePort);
        }
 
        // Clean up.
        CFRelease(threadPortName);
        CFAllocatorDeallocate(NULL, buffer);
    }
    else {
        // Process other messages.
    }
 
    return NULL;
}

配置完主线程, 剩下的惟一工作就是让新创建的工作线程创建自己的端口并登录, 下面是工作线程的入口函数, 该函数获取主线程的 port name 并使用它创建回主线程的远程连接, 然后给自己创建一个 port 并添加到当前线程的 runloop 中, 并向主线程发送包含本地端口名称的消息.

Setting up the thread structures(设置线程结构) -- 方法在主线程创建新线程时调用.

OSStatus ServerThreadEntryPoint(void* param) {
    // Create the remote port to the main thread.
    // 创建主线程的远程 port
    CFMessagePortRef mainThreadPort;
    CFStringRef portName = (CFStringRef)param;
 
    mainThreadPort = CFMessagePortCreateRemote(NULL, portName);
 
    // Free the string that was passed in param.
    // 参数释放 
    CFRelease(portName);
 
    // Create a port for the worker thread.
    // 创建当前线程 port name
    CFStringRef myPortName = CFStringCreateWithFormat(NULL, 
                                                      NULL, 
                              CFSTR("com.MyApp.Thread-%d"), 
                                         MPCurrentTaskID());
 
    // Store the port in this thread’s context info for later reference.
    // 保存到当前线程的 context 以便之后使用
    CFMessagePortContext context = {0, mainThreadPort, NULL, NULL, NULL};
    Boolean shouldFreeInfo; // 标记是否需要释放
    Boolean shouldAbort = TRUE;
    
    // 创建 message port
    CFMessagePortRef myPort = CFMessagePortCreateLocal(NULL,
                myPortName,
                &ProcessClientRequest,
                &context,
                &shouldFreeInfo);
 
    if (shouldFreeInfo) {
        // Couldn't create a local port, so kill the thread.
        MPExit(0);
    }
 
    // 创建 source
    CFRunLoopSourceRef rlSource = CFMessagePortCreateRunLoopSource(NULL, 
                                                                 myPort, 
                                                                      0);
    if (!rlSource) {
        // Couldn't create a local port, so kill the thread.
        MPExit(0);
    }
 
    // Add the source to the current run loop.
    // 添加 source 到 runloop
    CFRunLoopAddSource(CFRunLoopGetCurrent(), 
                                    rlSource, 
                       kCFRunLoopDefaultMode);
 
    // Once installed, these can be freed.
    CFRelease(myPort);
    CFRelease(rlSource);
 
    // Package up the port name and send the check-in message.
    CFDataRef returnData = nil;
    CFDataRef outData;
    CFIndex stringLength = CFStringGetLength(myPortName);
    UInt8* buffer = CFAllocatorAllocate(NULL, stringLength, 0);
 
    CFStringGetBytes(myPortName,
    CFRangeMake(0,stringLength),
         kCFStringEncodingASCII,
                              0,
                          FALSE,
                         buffer,
                   stringLength,
                           NULL);
 
    outData = CFDataCreate(NULL, buffer, stringLength);
 
    // 发送消息请求
    CFMessagePortSendRequest(mainThreadPort, 
                            kCheckinMessage, 
                                    outData, 
                       0.1, 0.0, NULL, NULL);
 
    // Clean up thread data structures.
    CFRelease(outData);
    CFAllocatorDeallocate(NULL, buffer);
 
    // Enter the run loop.
    CFRunLoopRun();
}

一旦它进入运行循环,所有发送到线程端口的未来事件都由 ProcessClientRequest 函数处理。该函数的实现取决于线程执行的工作类型.

你可能感兴趣的:(Runloop)