--参考:(官网) https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/Introduction/Introduction.html + (中文翻译) http://www.cocoachina.com/bbs/read.php?tid=87592
Run loops 是线程相关的的基础框架的一部分。一个 run loop 就是一个事件处理的循环,用来不停的调度工作以及处理输入事件。使用 run loop 的目的是让你的线 程在有工作的时候忙于工作,而没工作的时候处于休眠状态。
Run loop 的管理并不完全自动的。你仍然需要设计你的线程代码在合适的时候启 动 run loop 并正确响应输入事件。Cocoa 和 Core Fundation 都提供了 run loop objects 来帮助配置和管理你线程的 run loop。你的应用程序不需要显式的创建这些对象(run loop objects);每个线程,包括程序的主线程都有与之对应的 run loop object。只有辅助线程(secondary threads)才需要显式(explicitly)的运行它的 run loop。在 Carbon 和 Cocoa 程序中, 主线程会自动创建并运行它 run loop,作为一般应用程序启动过程的一部分。
以下各小节提供更多关于run loops内容以及如何为你的应用程序配置它们。关于 run loop object 的额外信息,参阅 NSRunLoop Class Reference 和 CFRunLoop Reference。
Run loop 本身听起来就和它的名字很像。它是一个循环,你的线程进入并使用它来运行事件处理(event handlers)响应输入事件。你的代码要提供实现循环部分的控制语句,换 言之就是要有 while 或 for 循环语句来驱动 run loop。在你的循环中,使用 run loop object to "run” the event-processing code that receives events and calls the installed handlers。
Run loop 接收输入事件来自两种不同的来源:输入源(input source)和定时源 (timer source)。输入源传递异步事件,通常消息来自于其他线程或程序。定时源则传递同步事件,发生在特定时间或者重复的时间间隔。两种源都使用程序的某一特定的处理例程(application-specific handler routine)来处理到达的事件。
下图显示了 run loop 的概念结构以及各种源。输入源传递异步消息给相应的handlers,并触发 runUntilDate:方法来退出(被线程里面associated NSRunLoop 对象调用)。定时源则直接传递消息给handler routines,但并不会引起 run loop退出。
除了处理输入源,run loops 也会生成关于run loop行为的通知 (notifications)。注册的 run loop的观察者(run-loop Observers)可以收到这些通知, 并在线程上面使用它们来做额外的处理。你可以使用 Core Foundation 在你的线程注册 run-loop 观察者。
下面将介绍更多关于 run loop 的构成,以及其运行的模式。同时也提及在处理事件中不同时间生成的通知。
Run loop 模式是所有要监视的输入源和定时源(a collection of input sources and timers to be monitored)以及要通知的run loop注册观察者(a collection of run loop observers to be notified)的集合。每次运行你的 run loop,你都要指定(无论显示还是隐式)其运行个模式。在 run loop 运行过程中,只有和模式相关的源才会被监视并允许他们传递事件消息。(同样的,只有和模式相关的观察者会收到run loop 的通知)。和其他模式关联的源只有在run loop运行在相应模式下才会运行,否则处于暂停状态。
通常在你的代码中,你可以通过指定名字来标识模式。Cocoa 和 Core foundation 定义了一个默认的和一些常用的模式,在你的代码中都是用字符串来标识这些模式。 当然你也可以给模式名称指定一个字符串来自定义模式。虽然你可以给模式指定任意名字,但是模式的内容则不能是任意的。你必须添加一个或多个输入源,定时源或者 run loop 的观察者到你新建的模式中让他们有价值。
通过指定模式可以使得 run loop 在某一阶段过滤来源于unwanted sources 的事件。大多数时候, run loop 都是运行在系统定义的默认模式上。但是模态面板(modal panel)可以运行在 “modal”模式下。在这种模式下,只有和模式面板相关的源才可以传递消息给线程。对于辅助线程,你可以使用自定义模式在周期性操作上屏蔽优先级低的源传递消息。
备注:模式区分基于事件的源而非事件的种类。例如,你不可以使用模式只选择处理鼠标按下或者键盘事件。你可以使用模式监听不同的端口,暂停定时器或者改变其他源和当前模式下处于监听状态 run loop 观察者
下表列出了 Cocoa 和 Core Foundation 定义的标准模式,并且介绍何时使用他 们。名称列出了你用来在你代码中指定模式实际的常量。
mode | name | description |
Default | NSDefaultRunLoopMode(Cocoa) kCFRunLoopDefaultMode (Core Foundation) |
The default mode is the one used for most operations. Most of the time, you should use this mode to start your run loop and configure your input sources. |
Connection | NSConnectionReplyMode(Cocoa) | Cocoa uses this mode in conjunction with NSConnection objects to monitor replies. You should rarely need to use this mode yourself |
Modal | NSModalPanelRunLoopMode(Cocoa) | Cocoa uses this mode to identify events intended for modal panels. |
Event tracking | NSEventTrackingRunLoopMode(Cocoa) | Cocoa uses this mode to restrict incoming events during mouse-dragging loops and other sorts of user interface tracking loops. |
Common modes | NSRunLoopCommonModes(Cocoa) kCFRunLoopCommonModes (Core Foundation) | This is a configurable group of commonly used modes. Associating an input source with this mode also associates it with each of the modes in the group. For Cocoa applications, this set includes the default, modal, and event tracking modes by default. Core Foundation includes just the default mode initially. You can add custom modes to the set using the CFRunLoopAddCommonMode function |
输入源异步的传送事件(deliver events asynchronously)给你的线程。事件来源取决于输入源的种类:基于端口的输入源(port-based input sources)和自定义输入源(Custom input sources )。基于端口的输入源监听程序相应的端口。自定义输入源则监听自定义的事件源。至于run loop,它不关心输入源的是基于端口的输入源还是自 定义的输入源。系统会实现两种输入源供你使用。两类输入源的区别在于如何发出信好(signaled): 基于端口的输入源由内核自动发送,而自定义的则需要人工从其他线程发送。
 当你创建输入源,你需要将其分配给 run loop 中的一个或多个模式。模式影响着输入源是否被监听。大多数情况下,run loop 运行在默认模式下,但是你也 可以使其运行在自定义模式。若某一源在当前模式下不被监听,那么任何其生成的事件将会被held,直到 run loop 运行在其关联的模式下。下文讲述不同输入源:
----基于端口的输入源:
Cocoa 和 Core Foundation 内置支持使用 端口相关的对象和函数 来创建端口输入源。例如,在 Cocoa 里面你从来不需要直接创建输入源。你只要简单的创建端口对象(port object),并使用 NSPort 的方法把该端口添加到 run loop。端口对象会自己处理创建和配置输入源。
在 Core Foundation,你必须人工创建端口和它的run loop 源.在两种情况下, 你都可以使用端口相关的函数(CFMachPortRef,CFMessagePortRef,CFSocketRef) 来创建合适的对象。更多关于如何设置和配置一个自定义端口源,参阅“配置一个基于端口的输入源”小节
----自定义输入源:
为了创建自定义输入源,必须使用 Core Foundation 里面的 CFRunLoopSourceRef 类型相关的函数来创建。你配置自定义输入源通过使用几个回调函数。Core Fundation 会在不同地方调用回调函数,配置源、处理输入事件同时销毁源当它从 run loop 移除的时候。
除了定义在事件到达时自定义输入源的行为,你也必须定义消息传递机制。这部分源运行在单独的线程里面,给输入源提供数据,并且在数据等待处理的时候发送信号。消息传递机制的定义取决于你,但最好不要过于复杂。
关于创建自定义输入源的例子,参阅“定义一个自定义输入源”小节。关于自定义输入源的信息,参考 CFRunLoopSource Reference
----Cocoa 执行 Selector 的源(Cocoa Perform Selector Sources):
除了基于端口的源,Cocoa 定义了自定义输入源,允许你在任何线程执行selector。和基于端口的源一样,执行 selector 请求会在目标线程上序列化,减缓在线程上多个方法同时运行引起的同步问题。不同于基于端口的源的是,一个 selector 执行完后会自动从 run loop 里面移除。
注意:在 Mac OS X v10.5 之前,执行selector 多半可能是给主线程发送消息,但是在 Mac OS X v10.5 及其之后和在 iOS 里面,你可以使用它们给任何线程发送消息。
当在其他线程上面执行 selector 时,目标线程须有一个活动的 run loop。对于 你创建的线程,这意味着线程在你显式的启动 run loop 之前处于等待状态。由于主线程自己启动它的 run loop,那么一旦程序通过委托调用 applicationDidFinishlaunching:的时候你将要调用那个线程。Run loop通过每次循环来处理所有队列的 selector的调用,而不是通过 loop 的迭代来一个个处理。
下表列出了在NSObject 中定义的可在其它线程执行的 selector。由于这些方法时定义 在 NSObject 中,你可以在任何可以访问 Objective-C 对象的线程里面使用它们,包 括 POSIX 的所有线程。这些方法实际上并没有创建新的线程去执行selector。
performSelectorOnMainThread: withObject: waitUntilDone: performSelectorOnMainThread: withObject:waitUntilDone:modes: |
Performs the specified selector on the application’s main thread during that thread’s next run loop cycle. These methods give you the option of locking the current thread until the selector is performed. |
performSelector: onThread:withObject:waitUntilDone: performSelector: onThread:withObject:waitUntilDone:modes: |
Performs the specified selector on any thread for which you have an NSThread object. These methods give you the option of blocking the current thread until the selector is performed. |
performSelector: withObject: afterDelay: performSelector: withObject:afterDelay:inModes: |
Performs the specified selector on the current thread during the next run loop cycle and after an optional delay period. Because it waits until the next run loop cycle to perform the selector, these methods provide an automatic mini delay from the currently executing code. Multiple queued selectors are performed one after another in the order they were queued. |
cancelPreviousPerformRequestsWithTarget: cancelPreviousPerformRequestsWithTarget:selector:object: |
Lets you cancel a message sent to the current thread using the performSelector: withObject: afterDelay: or performSelector: withObject: afterDelay:inModes: method. |
- (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. 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); }When configuring the run loop for a long-lived thread, it is better to add at least one input source to receive messages. Although you can enter the run loop with only a timer attached, once the timer fires, it is typically invalidated, which would then cause the run loop to exit. Attaching a repeating timer could keep the run loop running over a longer period of time, but would involve firing the timer periodically to wake your thread, 这实际上是轮询(polling)的另一种形式而已. By contrast, an input source waits for an event to happen, keeping your thread asleep until it does.
- (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. 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. }可以递归的运行run loop。换句话说你可以使用CFRunLoopRun,CFRunLoopRunInMode或者任一NSRunLoop的方法在输入源或定时器的处理程序里面启动run loop。这样做的话,你可以使用任何模式启动嵌套的run loop,包括被外层run loop使用的模式。
@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. @interface RunLoopContext : NSObject { CFRunLoopRef runLoop; RunLoopSource* source; } @property (readonly) CFRunLoopRef runLoop; @property (readonly) RunLoopSource* source; - (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop; @end尽管使用Objective-C代码来管理 the custom data of the input source, attaching the input source to a run loop requires C-based callback functions。当你正在把你的run loop源附加到run loop的时候,使用下面代码中的第一个函数(RunLoopSourceScheduleRoutine)。因为这个输入源只有一个客户端client(即主线程),它使用调度函数(scheduler function) to send a message to register itself with the application delegate on that thread. When the delegate wants to communicate with the input source, it uses the information in RunLoopContext object to do so.
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode) { RunLoopSource* obj = (RunLoopSource*)info; AppDelegate* del = [AppDelegate sharedAppDelegate]; RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl]; [del performSelectorOnMainThread:@selector(registerSource:) withObject:theContext waitUntilDone:NO]; }一个最重要的callback routines is the one used to process custom data when your input source is signaled。下面代码shows the perform callback routine associated with the RunLoopSource object。这个函数只是简单的发送了一个请求to do the work通过调用sourceFired 方法。然后继续处理在命令缓存区出现的命令。
void RunLoopSourcePerformRoutine (void *info) { RunLoopSource* obj = (RunLoopSource*)info; [obj sourceFired]; }如果你使用CFRunLoopSourceInvalidate函数把输入源从run loop里面移除的话,the system calls your input source’s cancellation routine。You can use this routine to notify clients that your input source is no longer valid and that they should remove any references to it。下面代码shows the cancellation callback routine registered with the RunLoopSource object. This function sends another RunLoopContext object to the application delegate, but this time asks the delegate to remove references to
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode) { RunLoopSource* obj = (RunLoopSource*)info; AppDelegate* del = [AppDelegate sharedAppDelegate]; RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl]; [del performSelectorOnMainThread:@selector(removeSource:) withObject:theContext waitUntilDone:YES]; }
- (id)init { CFRunLoopSourceContext context = {0, self, NULL, NULL, NULL, NULL, NULL,&RunLoopSourceScheduleRoutine,RunLoopSourceCancelRoutine,RunLoopSourcePerformRoutine}; runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context); commands = [[NSMutableArray alloc] init]; return self; } - (void)addToCurrentRunLoop { CFRunLoopRef runLoop = CFRunLoopGetCurrent(); CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode); }
- (void)registerSource:(RunLoopContext*)sourceInfo; { [sourcesToPing addObject:sourceInfo]; } - (void)removeSource:(RunLoopContext*)sourceInfo { id objToRemove = nil; for (RunLoopContext* context in sourcesToPing) { if ([context isEqual:sourceInfo]) { objToRemove = context; break; } } if (objToRemove) [sourcesToPing removeObject:objToRemove]; }注意:该回调函数调用了之前代码中描述的方法。
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop { CFRunLoopSourceSignal(runLoopSource); CFRunLoopWakeUp(runloop); }备注:你不应该试图通过自定义输入源处理一个SIGHUP或其他进程级别类型的信号。Core Foundation唤醒run loop的函数不是信号安全的,不能在你的应用信号处理例程里面使用。关于更多信号处理例程(signal handler routines),参阅sigaction主页。
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];下面代码 显示了使用Core Foundation函数来配置定时器的代码。尽管这个例子中并没有把任何用户定义的信息作为上下文结构( user-defined information in the context structure),但是你可以使用这个上下文结构传递任何你想传递的信息给定时器。关于该结构的内容的详细信息,参阅 CFRunLoopTimer Reference
CFRunLoopRef runLoop = CFRunLoopGetCurrent(); CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL}; CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0,0,&myCFTimerCallback, &context); CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);
- (void)launchThread { 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. [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode]; // Detach the thread. Let the worker release the port. [NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:) toTarget:[MyWorkerClass class] withObject:myPort]; } }为了在你的线程间建立双向的通信,you might want to have the worker thread send its own local port to your main thread in a check-in message. Receiving the check-in message lets your main thread know that all went well in launching the second thread and also gives you a way to send further messages to that thread。
#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
+(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]; [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]; }当使用NSMachPort时候,本地和远程线程可以使用相同的端口对象在线程间进行单边通信。换句话说,the local port object created by one thread becomes the remote port object for the other thread。
// 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]]; } }
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];