Runloop(一)概念

概述

  • run loop是与线程相关联的基础架构的一部分。

  • run loop是一个处理事件的循环(event processing loop),用于 schedule work(调度工作) 和 协调传入事件的接收。

  • run loop 的目的是让线程在有工作要做时保持忙碌,在没有工作要做时让线程进入休眠。

  • run loop管理并非完全自动。我们仍然必须设计线程的代码,以在适当的时候启动 run loop 并 响应传入的事件。

  • 每个线程(包括应用程序的主线程)都有一个关联的 run loop 对象,因此在应用程序中不需要显式地创建这些对象。

  • 在应用程序启动过程中,应用程序框架会在主线程上自动设置并运行run loop。不过,子线程(辅助线程)需要显式地运行它们的run loop

  • CocoaCore Foundation都提供了run loop对象(NSRunLoopCFRunLoop),用于配置和管理线程的run loop

Sources

run loop和它的名字听起来很像,它是线程进入的一个循环,用于运行event handlers(事件处理程序)以响应传入事件。

你的代码提供了用于实现Run Loop的 实际循环部分的控制语句,换句话来说,你的代码提供了 while或for循环 来驱动run loop。在循环中,你可以使用 run loop 对象来运行事件处理代码,以接收事件 并 调用已安装的处理程序(installed handlers)。

run loop 从两种不同类型的源(source) 接收事件:

  • 输入源(Input sources) 传递异步事件,通常是来自另一个线程或其他应用程序的消息。

  • 计时器源(Timer sources) 传递同步事件,这些事件在预定时间或重复间隔发生。

以上两种类型的源(source)在事件到达时都使用 特定于应用程序的处理程序例程(application-specific handler routine) 来处理事件。

如下图展示了 run loop 和各种源(source)的概念结构:

1、输入源(Input sources) 将异步事件传递给相应的处理程序,并使 runUntilDate: 方法(在线程关联的NSRunLoop对象上调用)退出。

2、计时器源(Timer sources) 将事件传递给它们的处理程序例程(handler routine),但不会导致run loop退出。

run-loop除了处理输入源(Input sources),它还生成有关run-loop行为的通知。注册 run-loop observers(运行循环观察者) 可以接收这些通知,并使用它们对线程执行其它处理。可以使用Core Foundation在线程上安装run-loop observers

输入源(Input Sources)

Input Sources 以异步方式向线程传递事件。事件的来源取决于Input Sources的类型,Input Sources的类型通常是如下这两种类型之一:

  • 基于端口的输入源(Port-based input sources),监控应用程序的Mach端口,基于端口的源由内核自动发出信号。

  • 自定义输入源(Custom input sources),监控自定义事件源,自定义源必须从另一个线程手动发出信号。

就你的 run loop 而言,input source是 基于端口 还是 自定义的 并不重要。系统通常实现两种类型的输入源,您可以按原样使用。两种信号源之间的唯一区别是信号的发送方式。

创建Input Sources时,可以将它分配给run loop的一种或多种 ModesModes 会影响在任何给定时刻监控哪些 Input Sources。如果Input Sources不在当前监视的模式下,则它生成的任何事件都将保留,直到run loop以正确的 Mode 运行。

以下各节描述了一些输入源:

Port-Based Sources

CocoaCore Foundation 为使用与端口相关的对象和函数创建 Port-Based Sources提供了内置支持。

例如,在 Cocoa 中,根本不需要直接创建输入源(Input Sources)。您只需创建一个NSPort对象,并使用NSPort的方法将该端口添加到run loop中。NSPort对象为您处理所需输入源的创建和配置。

Core Foundation中,必须手动创建端口及其运行循环源。在这两种情况下,都使用与端口不透明类型(CFMachPortRef、CFMessagePortRef或CFSocketRef)关联的函数来创建适当的对象。

Custom Input Sources

必须使用与Core Foundation中的CFRunLoopSourceRef不透明类型关联的函数来创建Custom Input Sources

可以使用多个回调函数配置Custom Input SourcesCore Foundation在不同的点调用这些函数来配置源、处理任何传入事件,并在源(Sources)从 run loop中删除时将其销毁。

除了定义事件到达时自定义源的行为外,还必须定义事件传递机制。Sources的这一部分运行在一个单独的线程上,负责向输入源提供其数据,并在数据准备好进行处理时向输入源发出信号。事件传递机制由您决定,但不必过于复杂。

Cocoa Perform Selector Sources

除了port-based sourcesCocoa还定义了一个自定义输入源,该 Sources 允许你在 任何线程上执行 Selector

  • port-based source 一样,perform selector请求在目标线程上序列化(serialized),从而缓解了在一个线程上运行多个方法时可能出现的许多同步问题。

  • Port-Based Sources不同的是,Perform Selector Sources在执行其 Selector 后会将自身从run loop中移除。

在另一个线程上执行选择器(Selector)时,目标线程必须具有活动的run loop对于你创建的线程,这意味着等到你的代码显式地启动run loop。但是,因为主线程启动了自己的run loop,因此只要应用程序调用AppDelegateapplicationDidFinishLaunching:方法时,你就可以立即对该线程发出调用。每次循环时,run loop都会处理所有排队的perform selector的调用,而不是在每次循环迭代中处理一个。

Perform Selector 的方法都是在NSObject上声明的(NSObjectNSThreadPerformAdditionsNSDelayedPerforming 分类中声明),可以在任何可以访问Objective-C对象的线程中使用它们,这些方法并不创建新线程来执行selector,仅在你指定的或当前的开启了 run loop 的线程中执行。

如下所示消息选择器方法,具体可以参考NSObject Class Reference

在应用程序主线程的下一个 run loop cycle 中执行指定的 Selector。这些方法提供了在执行 selector 之前阻塞当前线程的选项,即 waitUntilDone 参数表示是否等待 selector 执行完成再执行接下来的语句。

1
2
func performSelector(onMainThread aSelector: Selector, with arg: Any?, waitUntilDone wait: Bool)
func performSelector(onMainThread aSelector: Selector, with arg: Any?, waitUntilDone wait: Bool, modes array: [String]?)

在拥有NSThread对象的线程上执行指定的 Selector。这些方法提供了在执行选择器之前阻塞当前线程的选项。

1
2
func perform(_ aSelector: Selector, on thr: Thread, with arg: Any?, waitUntilDone wait: Bool)
func perform(_ aSelector: Selector, on thr: Thread, with arg: Any?, waitUntilDone wait: Bool, modes array: [String]?)

在下一个 run loop cycle 和一个可选的延迟周期之后,在当前线程上执行指定的 Selector
因为它要等到下一个run loop cycle才执行Selector,所以这些方法提供了当前执行代码的一个自动最小延迟。多个排队的选择器按照它们排队的顺序依次执行。

1
2
func perform(_ aSelector: Selector, with anArgument: Any?, afterDelay delay: TimeInterval)
func perform(_ aSelector: Selector, with anArgument: Any?, afterDelay delay: TimeInterval, inModes modes: [RunLoop.Mode])

用于取消使用 perform(_:, with:, afterDelay:)perform(_:, with:, afterDelay:, inModes:) 方法发送到当前线程的消息。

1
2
class func cancelPreviousPerformRequests(withTarget aTarget: Any)
class func cancelPreviousPerformRequests(withTarget aTarget: Any, selector aSelector: Selector, object anArgument: Any?)

Timer Sources

Timer Sources 在将来预先设定的时间同步向线程传递事件。Timers 是线程通知自身执行某项操作的一种方式。例如,搜索字段时,在用户连续按键之间经过一定的时间后,可以使用定时器启动自动搜索。使用这个延迟时间可以使用户在开始搜索之前尽可能多地输入所需的搜索字符串。

  • 尽管 timer 产生基于时间的通知,但它不是一种 实时机制

input sources一样,timerrun loop 的特定模式(mode)相关联。如果 timer 不在 run loop 当前监控的 Mode 下,则在 timer 支持的Mode下运行run loop之前,timer不会触发。类似地,如果在 run loop正在执行handler routine(处理程序例程)时触发timer,则 timer 将等待下一次通过 run loop 调用其handler routine。如果 run loop 根本没有运行,则timer永远不会触发。

  • 可以将 timer 配置为只生成一次事件 或 重复生成事件。

重复 timer 根据预定的触发时间自动重新调度自己,而不是实际的触发时间。例如,如果一个timer被安排在一个特定的时间点触发,并且在此之后每隔5秒触发一次。则即使实际触发时间被延迟,计划的触发时间也将始终落在原来的5秒时间间隔上。如果触发时间延迟太短,以至于错过了一个或多个计划的触发时间,那么对于错过的时间段,timer只触发一次。在为错过的时间段触发之后,timer将被重新安排为下一个预定的触发时间。

Run Loop Observers

sources 不同,sources在发生适当的异步或同步事件时触发的,run loop observersrun loop本身执行期间的特殊位置触发。

可以使用run loop observers来准备线程处理给定的事件,或者在线程进入休眠状态之前准备线程。可以将run loop observersrun loop中的以下事件关联起来:

  • 进入run loop
  • run loop准备处理计时器时
  • run loop准备处理输入源时
  • run loop即将进入休眠状态时
  • run loop已唤醒时,但在它处理唤醒它的事件之前。
  • 退出run loop

可以使用Core Foundation向应用程序添加run loop observers。与 Timer 类似,run loop observers 可以使用一次,也可以重复使用。一次性观察者在触发后从run loop中删除,而重复的观察者保持附加状态。

Run Loop Modes

运行循环模式(run loop mode) 是要监控 Input sourcesTimer sources 的集合,以及要通知的run-loop observers的集合。

  • 每次运行run loop时,需要(显式或隐式)指定运行的特定模式(Modes)。在代码中,通过字符串标示Modes

  • run loop的传递过程中,只有与该Modes关联的sources才会被监控,并允许其传递事件。同样,只有与该模式(Modes)关联的观察者(observers)才会收到run loop进度的通知。与其他Modes相关联的sources将保留任何新事件,直到后续事件以适当的Modes通过循环。

CocoaCore Foundation都定义了一个默认模式和几种常用模式,以及用于在代码中指定这些模式的字符串。

自定义模式(custom modes):只需为模式名称指定一个自定义字符串就可以自定义模式(custom modes)。另外,必须确保将一个或多个Input sourcesTimer sourcesrun-loop observers添加到您创建的任何模式(mode)中,从而使它们有用。

在通过run loop的特定过程中,可以使用mode过滤掉不需要的sources中的事件。大多数情况下,我们以系统定义的默认模式下运行run loop。然而,modal panel可能会以modal mode运行。在这种mode下,只有与modal panel相关的sources才会向线程传递事件。对于子线程,可以使用custom modes来防止低优先级源(low-priority sources)在时间紧迫的操作期间传递事件。

注意:Modes 根据事件的source而不是事件类型进行区分。例如:您不会使用modes仅匹配鼠标按下事件或仅匹配键盘事件,可以使用 modes 监听一组不同的端口、暂时挂起timers,或以其他方式更改当前正在监视的sourcesrun loop observers

CocoaCore Foundation 定义的标准模式:

模式 模式常量 描述
Default NSDefaultRunLoopMode (Cocoa)
kCFRunLoopDefaultMode (Core Foundation)
默认模式是大多数操作使用的模式,用于处理NSConnection对象以外的输入源的模式。通常,您应该使用此模式来启动run loop和配置Input sources
Connection NSConnectionReplyMode (Cocoa) Cocoa将此模式与NSConnection对象结合使用来监视响应。很少使用
Modal NSModalPanelRunLoopMode(Cocoa) Cocoa使用此模式识别用于modal panels(如NSSavePanelNSOpenPanel)的事件。
Event tracking NSEventTrackingRunLoopMode (Cocoa) Cocoa使用此模式在鼠标拖动循环和其他类型的用户界面跟踪循环期间限制传入事件。
Common modes NSRunLoopCommonModes (Cocoa)
kCFRunLoopCommonModes (Core Foundation)
这是一组可配置的常用模式。将输入源与此模式相关联还会将其与组中的每个模式相关联。在Cocoa应用程序中,此集合默认包括default、modal、event tracking 模式。Core Foundation最初只包含默认模式。您可以使用CFRunLoopAddCommonMode函数向该设置添加自定义模式。

事件的Run Loop执行顺序(Run Loop Sequence of Events)

每次运行时,线程的run loop都会处理挂起(未解决、待处理)的事件,并为任何附加的观察者(observers)生成通知。其具体执行顺序如下所示:

1、通知Observers已进入run loop
2、通知Observers任何准备就绪的定时器(timer)即将触发。
3、通知Observers任何不基于端口的输入源(Port-Based Sources)都将被触发。
4、触发所有准备触发的非基于端口的输入源(non-port-based input sources)。
5、如果基于端口的输入源(port-based input source)已准备好并等待启动,请立即处理事件。 转到步骤9。
6、通知Observers线程即将进入休眠。
7、将线程置于休眠状态,直到一下任一事件发生:

  • 基于端口的输入源(Port-Based Sources)事件到达。
  • timers触发
  • run loop设置的超时时间已超时。
  • run loop被显式地唤醒。

8、通知观察者线程刚刚被唤醒
9、处理挂起(未解决)的事件

  • 如果触发用户定义的timer,请处理timer事件并重新启动run loop。转到步骤2。
  • 如果触发了输入源(input source),请传递事件。
  • 如果run loop被显式地唤醒,但还没有超时,请重新启动run loop。转到步骤2。

10、通知观察者run loop已退出。

由于timerinput sourcesobserver notifications是在事件实际发生之前发送的,因此通知的时间和实际事件发生的时间之间可能会有间隔。如果这些事件之间的时间间隔非常关键,那么可以使用 sleepaweak from sleep 通知来帮助您关联实际事件之间的时间。

由于timer和其他周期性事件是在运行run loop时传递的,因此绕过该循环会中断这些事件的传递。这种行为的典型示例是,每当您通过进入循环并从应用程序反复请求事件来实现鼠标跟踪例程时,都会发生这种行为。因为您的程式码直接抓取事件,而不是让应用程式正常调度这些事件,所以只有在的鼠标跟踪例程(mouse-tracking routine)退出并将控制权返回应用程式之后,活动timer才能触发。

可以使用run loop对象显式唤醒run loop。其他事件也可能导致run loop被唤醒。 例如,添加另一个non-port-based input source会唤醒run loop,以便可以立即处理input source,而不是等到发生其他事件为止。

什么时候使用 Run Loop?

只有在为应用程序创建子线程时才需要显式运行run loop。应用程序主线程的run loop是基础架构的重要组成部分。应用程序框架提供了用于运行主应用程序循环的代码,并自动启动该循环。 在iOSUIApplicationrun 方法作为正常启动序列的一部分,启动应用程序的主循环。

对于辅助线程,您需要确定是否需要run loop,如果需要,请自行配置并启动它。 在所有情况下,您都无需启动线程的run loop。 例如,如果使用线程执行一些长时间运行的预定任务,则可以避免启动run looprun loop用于需要与线程进行更多交互的情况。 例如,如果您打算执行以下任一操作,则需要启动run loop

  • 使用Port-Based SourceCustom Input Sources与其他线程进行通信。

  • 在线程上使用 timers

  • Cocoa应用程序中使用任何performSelector…方法。

  • 保持线程绕执执行periodic tasks(定期任务)。

如果选择使用 run loop,则配置和设置很简单。不过,与所有线程编程一样,你应该有一个在适当情况下退出子线程的计划。通过让线程退出而干净地结束它总是比强制它终止要好。

学习博客

Threading Programming Guide

YYKit大佬 深入理解RunLoop

Rimson * Runloop

掌握子线程RunLoop生命周期

源码解读RunLoop

iOS刨根问底-深入理解RunLoop

简书 石头89 RunLoop 01 - 原理

QiShare * iOS RunLoop(一)

MMao * RunLoop究竟是怎么运作的

Energy Efficiency Guide for iOS Apps


至此,以上是Apple目前关于 Runloop的基本概念解释,另外我也参考了 Apple过期文档:About Run LoopsIntroduction to Run Loops,基本描述如下。可以省略不看。

关于 run loop

Core Foundation使用 CFRunLoop 不透明类型(opaque type)为每个应用程序的事件循环(event loop)提供基础。

CFRunLoop对象监视表示任务的各种输入源的对象。当输入源(input source)准备好进行处理时,run loop将调度控制(dispatches control)。

输入源(input source)的示例可能包括:用户输入设备、网络连接、周期性或延时事件以及异步回调。输入源(input source)已在run loop中注册,并且当 run looprun 时,在发生某些活动时将调用与每个 输入源(input source) 关联的回调函数。

在运行时,run loop会经历一个活动周期,检查输入源(input source),触发需要触发的计时器(timers),然后run loop blocks等待某些事情发生(或者在计时器的情况下,等待某些事情发生的时间)。当发生某些事情时,run loop将唤醒,处理活动(通常通过调用输入源(input source)的回调函数,检查其他源,触发计时器,然后返回睡眠状态 等等。

每个线程的 run loop 都会监控自己的独立对象列表。例如,在Carbon或Cocoa应用程序中,主线程的run loop 通常监控用户生成的所有事件。其他线程可能会使用其 run loop 来监听(然后处理)网络活动,接收来自其他线程或进程的消息,或执行定期活动。通过将这些输入源放置在不同的run loop中,可以在不阻塞任何其他线程的run loop的情况下处理事件,例如处理主线程的run loop,它处理用户事件。

可以将三种类型的对象放置到 run loop 中并由其监视:sources, timers, 和 observers

Sources

运行循环源(run loop sources),由CFRunLoopSource不透明类型(opaque type)表示,是可以放入 run loop 的输入源(input sources)的抽象。输入源(input sources)通常生成异步事件,例如 到达网络端口的消息 或 用户执行的操作。

输入源类(input sources)型通常定义一个API,用于创建和操作该类型的对象,就好像它是与run loop独立分开的实体一样,然后提供一个函数来为对象创建CFRunLoopSource

可以将 运行循环源(run loop sources)注册到 run loop 中,并充当 run loop 和 实际输入源类型 对象之间的中介。输入源的示例包括 CFMachPortCFMessagePortCFSocket

Timers

运行循环计时器(Run loop timers),由 CFRunLoopTimer 不透明类型(opaque type)表示,是专用的运行循环源(run loop sources),在将来的预设时间触发。定时器可以只发射一次,也可以按固定的时间间隔重复发射。重复定时器也可以手动调整下一次触发时间。

Observers

运行循环观察者(Run loop observers),由 CFRunLoopObserver 不透明类型(opaque type)表示,提供了在 run loop 中的不同点接收回调的通用方法。

和异步事件发生时触发的源 和 定时器在特定时间经过时触发的源不同,观察者(observers)在 run loop 执行过程中的特殊位置触发,例如在处理源之前 或 run loop 进入睡眠状态之前,等待事件发生。

本质上,观察者(observers)是专门的 run loop 源,表示 run loop 本身中的事件。

输入模式 (Input Modes)

模式(Modes)用任意的字符串名称标识,它有一组源(sources)、计时器(timers)、观察者(observers)与之相关联。

每个 run loop 可以有不同的模式(Modes),并在其中运行。run loop以命名模式(Modes)运行,以使其监视在该模式(Modes)下注册的对象。

模式(Modes)的示例包括:默认模式(default mode)(进程通常大部分时间都会使用该模式)和 模态面板模式(modal panel mode)。

模式(Modes)不提供粒度(granularity)。例如,用户输入事件的有趣类型。这种更细粒度的粒度由更高级别的框架提供,如Cocoa和Carbon,具有“获取下一个事件匹配掩码”或类似的功能。

要在run loop sourcetimerobserver需要处理时接收回调,您必须首先使用适当的 CFRunLoopAdd... 函数将对象置于run loop mode。稍后可以使用适当的 CFRunLoopRemove... 函数 或 使对象无效化,从run loop mode中删除对象,以停止接收其回调。

文章作者: Czm
文章链接: http://yoursite.com/2020/10/12/Runloop-%E4%B8%80/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Czm