文章目录
前言
本篇主要是对官方文档和CFRunLoop源码的学习。
一、RunLoop解析
RunLoop是线程进入的一个事件处理循环
,用于响应传入事件并运行事件处理程序。
RunLoop的目的是在有工作要做时让线程忙,而在没有工作时让线程进入睡眠状态。
每个线程都有一个关联的RunLoop对象,但只有辅助线程需要显式地运行其运行循环。在应用程序启动过程中应用程序框架会自动在主线程上设置并运行运行循环。
RunLoop接收两种类型的Source:
- Input Sources,传递异步事件。将异步事件传递给相应的处理程序,并导致runUntilDate:方法(在线程的关联NSRunLoop对象上调用)退出
- Timer Sources,传递同步事件。将事件传递到其处理程序例程,但不会导致RunLoop退出。
除了处理输入的source,RunLoop还会生成相关行为的通知。
1.1 Modes
RunLoop Mode是要监视的Input source和Timer的集合,以及要通知的RunLoop Observer的集合。
每次运行RunLoop时,都要(显式或隐式)指定运行的特定“模式”。
在RunLoop的整个过程中,仅监视与该模式关联的源,并允许其传递事件。 (类似地,仅将与该模式关联的观察者通知RunLoop的进度。)与其他模式关联的源将保留任何新事件,直到随后以适当的模式通过循环。
必须确保将一个或多个Input sources、Timer或RunLoop Observer添加到创建的任何模式中,如果RunLoop没有任何要监视的源,则当你尝试运行它时,它将立即退出。
模式根据事件的来源而不是事件的类型进行区分。
1.2 Sources
RunLoop接收两种类型的Source:
- Input Sources,传递异步事件
- Timer Sources,传递同步事件
1.2.1 Input Sources
输入事件源有两种:基于Port的和自定义的
。基于端口的输入源监视应用程序的Mach端口,自定义输入源监视事件的自定义源。两种信号源之间的唯一区别是信号的发送方式
。基于端口的源由内核自动发出信号,而自定义源必须从另一个线程手动发出信号。
- Port-Based Sources
Cocoa和Core Foundation提供了内置支持,用于使用与端口相关的对象和功能创建基于端口的输入源。NSPort、CFMachPortRef、CFMessagePortRef、 CFSocketRef等。 - Custom Input Sources
若要创建自定义输入源,必须在Core Foundation中使用与CFRunLoopSourceRef类型关联的功能。可以使用多个回调函数配置自定义输入源。Core Foundation在不同的位置调用这些函数来配置源,处理任何传入事件,从RunLoop中删除源时拆除源。
除了定义事件到达时自定义源的行为外,还必须定义事件传递机制
。源代码的这一部分在单独的线程上运行,负责为输入源提供其数据,并在准备好处理数据时向其发出信号。- Cocoa Perform Selector Sources
Cocoa定义的一个自定义输入源,该源可在任何线程上执行选择器。与基于端口的源类似,执行选择器请求在目标线程上序列化,从而减轻了在一个线程上运行多种方法时可能发生的许多同步问题。与基于端口的源不同,执行选择器源在执行选择器后将其自身从运行循环中删除。
在另一个线程上执行选择器时,目标线程必须具有活动的运行循环
。每次循环时,运行循环都会处理所有排队的执行选择器调用,而不是在每次循环迭代时都处理一个。这些perform方法实际上并不创建新的线程来执行选择器。
- Cocoa Perform Selector Sources
1.2.2 Timer Sources
计时器不是实时机制,它与RunLoop的特定模式相关联
。如果计时器不在RunLoop当前正在监视的模式下,则只有在以计时器支持的一种模式运行RunLoop后,计时器才会触发。同样,如果RunLoop在执行处理程序例程的中间触发计时器,则计时器将等到下一次通过RunLoop调用其处理例程。如果RunLoop根本没有运行,则计时器永远不会触发。
可以将计时器配置为仅一次或重复生成事件。重复计时器会根据计划的触发时间(而不是实际的触发时间)自动重新计划自身。
例如,如果计划将计时器在特定时间触发,然后每5秒触发一次,则即使实际触发时间被延迟,计划的触发时间也将始终落在原始的5秒时间间隔上。如果触发时间延迟得太多,以至于错过了一个或多个计划的触发时间,则计时器将在错过的时间段内仅触发一次。在错过了一段时间后触发之后,计时器将重新安排为下一个计划的触发时间。
1.3 Observers
Observers在RunLoop本身执行期间的特定位置触发。
可以将Observer与RunLoop中的以下事件相关联:
- 运行循环的
进入
- 当运行循环
将要处理计时器
时 - 当运行循环
将要处理输入源
时 - 当运行循环
即将进入睡眠
状态时 - 当运行循环
唤醒时,但在处理该事件之前将其唤醒
- 运行循环的
退出
1.4 事件执行顺序
每次运行它时,线程的RunLoop都会处理未决事件,并为所有关心的观察者生成通知。它的执行顺序如下所示:
- 通知Observers已进入RunLoop。
- 通知Observers任何准备就绪的Timer即将触发。
- 通知Observers任何不基于端口的Input sources都将被触发。
- 触发所有准备触发的非基于端口的输入源。
- 如果基于端口的输入源已准备好并等待启动,请立即处理事件。转到步骤9。
- 通知观察者线程即将进入睡眠状态
- 使线程进入睡眠状态,直到发生以下事件之一:
- 事件到达基于端口的输入源
- 计时器触发
- 为RunLoop设置的超时值到期
- RunLoop被显式唤醒
- 通知观察者线程刚刚醒来
- 处理未决事件
- 如果触发了用户定义的计时器,请处理计时器事件并重新启动循环。转到步骤2
- 如果触发了输入源,请传递事件
- 如果运行循环已显式唤醒,但尚未超时,请重新启动循环。转到步骤2
- 通知Observers已退出RunLoop。
1.5 何时使用RunLoop
主线程会自动运行,对于辅助线程,需要确定是否需要RunLoop,如果需要,请自行配置并启动它
。运行循环用于需要与线程进行更多交互
的情况,比如执行以下任一操作,则需要启动运行循环:
使用端口或自定义输入源与其他线程进行通信。
在线程上使用计时器
在Cocoa应用程序中使用任何performSelector…方法
保持线程执行定期任务
二、CFRunLoop源码
2.1 RunLoop对应的CF类
在 CoreFoundation 里面关于 RunLoop 有5个类: CFRunLoopRef、CFRunLoopModeRef(没对外暴露)、CFRunLoopSourceRef、CFRunLoopTimerRef、CFRunLoopObserverRef。
它们的关系如下:
一个 RunLoop 包含若干个 Mode,每个 Mode 包含若干个Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
2.1.1 CFRunLoopRef
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
//__CFRunLoop部分代码
struct __CFRunLoop {
......
_CFThreadRef _pthread; //对应的线程
CFMutableSetRef _commonModes; // common mode中包含的模式
CFMutableSetRef _commonModeItems; //加入到commonMode中的source、timer、observer
CFRunLoopModeRef _currentMode; //当前运行的模式
CFMutableSetRef _modes; //RunLoop中的模式
......
};
通过CFRunLoopCopyCurrentMode()和CFRunLoopCopyAllModes()函数可查看RunLoop的当前模式和modes中的所有模式。
2.1.2 CFRunLoopModeRef
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
//__CFRunLoopMode部分代码
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
......
};
2.1.3 CFRunLoopSourceRef
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopSource * CFRunLoopSourceRef;
struct __CFRunLoopSource