注: 本文对照
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
从两种不同类型的源接收事件:- 输入源, 传递异步事件, 通常来自另一个线程或另一个程序的消息, 如:
port
,custom
,performSelector:
等; - 定时器源
timer
, 提供同步事件, 发生在预定时间或重复间隔.
- 输入源, 传递异步事件, 通常来自另一个线程或另一个程序的消息, 如:
输入源(
Input sources
)将异步事件交付给相应的处理程序, 会使runUntilDate:
方法(在线程的关联NSRunLoop
对象上调用)无效, 即轮播图的问题;-
计时器源(
Timer sources
)将事件交付给它们的处理程序例程,但不会导致运行循环退出.
observers
,runloop
状态观察者,runloop
会给run loop observers
发送runloop
状态的通知, 使观察者在线程上执行额外的处理, 在CoreFoundation
中可以添加run loop observers
(CFRunLoopAddObserver()
).
Run Loop Modes
-
Run Loop Modes
是runloop
要监视的输入源和计时器源以及要通知的observers
的 集合. - 每次运行
runloop
时,都需要显示/隐式指定运行模式, 在runloop
循环过程中, 只监视与该模式相关的源, 并允许交付它们的事件, 并且只有与该模式关联的观察者才会被通知运行循环的进度(状态)。
释义:runloop
每次循环都有一个mode
, 只允许与该 mode 相关联的源才能把事件给runloop
处理,runloop
的循环进度也只有与该mode
相关联的观察者才会被通知. - 可以指定
runloop
运行mode
, 但必须添加一个或多个输入源,timer
或者观察者到这个mode
中, 才能使该mode
起效. -
runloop
使用mode
过滤来自不需要的源的事件, 大多数情况下你希望以系统定义的default mode
下运行, 但是模态面板下可能以 modalmode
运行, 在这种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(基于端口的输入源)
Cocoa
和CoreFoundation
为使用Port-related
(端口相关) 对象和函数创建Port-Based
输入源提供内联支持, 例如, 在Cocoa
中, 根本不需要直接创建输入源, 只需要创建一个端口对象并且使用NSPort
类的方法将port
添加到runloop
,port
对象处理输入源需要的创建和配置.
在CoreFoundation
中, 必须手动创建端口及其运行循环源.
在这两种情况下, 都是通过使用与端口不透明类型(CFMachPortRef
、CFMessagePortRef
或CFSocketRef
)相关的函数创建对象的.
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 的事件队列)
每次运行时,线程的运行循环处理等待的事件并为任何附加的观察者生成通知, 具体顺序如下:
- Notify observers that the run loop has been entered. -- 通知观察者
runloop
已经进入循环 - Notify observers that any ready timers are about to fire. -- 通知观察者
timers
将要触发事件 - Notify observers that any input sources that are not port based are about to fire. -- 通知观察者 不是基于端口的输入源 将要触发
- Fire any non-port-based input sources that are ready to fire. -- 通知观察者 不是基于端口的输入源 触发了
- If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9. -- 如果一个基于端口的输入源准备且等待触发, 立即处理该事件 ==> step 9.
- Notify observers that the thread is about to sleep. -- 通知观察者 线程将要休眠
- 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. -- 被显式地唤醒
- Notify observers that the thread just woke up. -- 通知观察者 线程刚刚被唤醒
- 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.
- If a user-defined timer fired, process the timer event and restart the loop. Go to step 2. -- 如果一个用户定义的
- Notify observers that the run loop has exited. -- 通知观察者
runloop
已经退出.
对应流程如下图:
由于计时器和输入源的观察者通知是在这些事件实际发生之前交付的,因此通知的时间和实际事件的时间之间可能存在差距。如果这些事件之间的时间非常关键,则可以使用
sleep
和sleep-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
, andrun-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<NSRunLoopMode> *)modes block:(void (^)(void))block
- Schedules the execution of a block on the target run loop in given modes.
-- 在runloop
指定mode
中执行block
- Schedules the execution of a block on the target run loop in given modes.
- - (void)performBlock:(void (^)(void))block;
- Schedules the execution of a block on the target run loop.
-- 在runloop
中执行block
- Schedules the execution of a block on the target run loop.
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
, 也就是说可以调用 CFRunLoopRun
、CFRunLoopRunInMode
或任何 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
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
函数, 可以在这个函数中通知客户端该输入源已经失效以让客户端移除对该源的引用, 下面的代码显示了RunLoopSource
的RunLoopSourceCancelRoutine
回调处理, 该函数给应用程序代理发送了又一个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)
如下代码显示了
RunLoopSource
的init
方法和addToCurrentRunLoop
方法,init
方法创建的CFRunLoopSourceRef
对象, 必须附加到runloop
中,RunLoopSource
会将自身作为上下文信息传递以至于回调函数可以用指针指向它, 直到工作线程调用addToCurrentRunLoop
方法才会添加源到runloop
, 同时会执行RunLoopSourceScheduleRoutine
回调, 一旦添加完成, 线程就会 run therunloop
等待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
的话, 会导致源事件处理被延时.
客户端准备好处理他们加到缓冲区里的命令时, 会调用RunLoopSource
的fireCommandsOnRunLoop
方法:
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
中, 也可以通过NSRunLoop
的addTimer: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 的输入源)
Cocoa
和CoreFoundation
都提供了基于端口的对象,用于线程或进程之间的通信.
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
对象以驱动线程执行,worker
的sendCheckinMessage:
方法为工作线程创建了一个本地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
对象监听来自工作线程的消息. 工作线程需要port
的name
来建立连接, 以便将字符串值传递给工作线程的入口函数,name
通常唯一, 否则会发生冲突.
Attaching a Core Foundation message port to a new thread(添加 CoreFoundation
的 message 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
函数处理。该函数的实现取决于线程执行的工作类型.