转载--->NSRunLoop(详解)
NSRunLoop大部分情况在多线程编程的时候才会用到。。但是一般不会用NSRunLoop,因为它不是线程安全的。一般都建议用CFRunLoop,这个是线程安全的。input source and port-based custom source这些操作,是向线程里面添加操作的。添加的这些操作,会在该线程执行空间的调度下执行。
通俗的理解就是如果你创建的了一个子线程,子线程的运行函数如下
- (void) subThread (void*)unused {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
CFRunLoopRun();
[pool release];
}
这么写,CFRunLoopRun()这个调用不会停住,必须保证有有事件在线程中执行,或者等待执行。。这就用到了input source这些方法。如果你得子线程执行完一个任务后就退出。你不需要得到或者关心这个子线程如何运行的话。iphone的建议是利用operatequeue.这个queue里面每一个task都是单独的一个线程。大部分情况是是每个线程只会执行一个任务。会造成不必要的线程开销。好处就是编码人员比较省事。
CFRunLoop 对象监控任务(task)的输入源,并在它们为处理做好准备的时候调度控制。输入源样例可能包括用户输入设备、网络链接、定期或时间延迟事件,还有异步回调。
有3类对象可以被run loop监控:sources、timers、observers。
当这些对象需要处理的时候,为了接收回调,首先必须通过 CFRunLoopAddSource
,CFRunLoopAddTimer
, or CFRunLoopAddObserver
把这些对象放入run loop。 要停止接收它的回调,你也可以稍候通过CFRunLoopRemoveSource从run loop中移除某个对象。
run loop有不同的运行模式,每种模式都有其自身的对象集,run loop监控,同时在该模式下运行。 Core Foundation定义了一种默认模式kCFRunLoopDefaultMode 来持有对象,在应用或线程闲置的时候这些对象应该被监控。当一个对象被添加到不认识的模式时,额外模式自动创建。每个run loop有它自己独立的模式集。
Core Foundation还定义了一个特殊的伪模式kCFRunLoopCommonModes 来持有应当被“common”模式集共享的对象。 通过调用CFRunLoopAddCommonMode 来添加一个模式到“common”模式集。 默认模式kCFRunLoopDefaultMode 总是common模式集中的一个成员。kCFRunLoopCommonModes
常数决不会传给CFRunLoopRunInMode 。每个run loop有它自己独立的common模式集。
每个线程恰好有一个run loop,既不可以创建,也不能销毁线程的run loop。,Core Foundation 根据需要为你创建。通过CFRunLoopGetMain 你可以获得当前线程的run loop。调用lCFRunLoopRun
来使当前线程的run loop以默认模式运行起 来,直到调用CFRunLoopStop 来停止run loop。你也可以调用CFRunLoopRunInMode 来使当前线程的run loop以指定模式运行起来一段时间或者直到run loop被停止。 run loop只能在请求模式至少有一个source或者timer可监控的情况下运行起来。
run loop可以递归运行,你可以在任何run loop 标注内部调用CFRunLoopRun
或 CFRunLoopRunInMode ,还可以创建嵌套run loop,并在当前线程调用栈激活,在标注内并没有限制在那种模式可以运行。
你可以创建另一个run loop,激活运行在任何可行的run loop模式,包括任何已经运行在调用堆栈中的更高的模式。
Cocoa 和 Carbon 每个都是建立在 CFRunLoop上来实现它们自己更高级别的事件循环。当编写一个 Cocoa 或者 Carbon 应用,你可以添加你的sources、timer和observers到它们的run loop对象中。你的对象将会作为常规应用事件循环的一部分来得到监控。使用 NSRunLoop 实例方法 getCFRunLoop
来获得对应应于cocoa run loop的CFRunLoop,在carbon应用中使用 GetCFRunLoopFromEventLoop
函数.Cocoa 和 Carbon 每个都是建立在 CFRunLoop上来实现它们自己更高级别的事件循环。当编写一个 Cocoa 或者 Carbon 应用,你可以添加你的sources、timer和observers到它们的run loop对象中。你的对象将会作为常规应用事件循环的一部分来得到监控。使用 NSRunLoop 实例方法 getCFRunLoop
来获得对应应于cocoa run loop的CFRunLoop,在carbon应用中使用 GetCFRunLoopFromEventLoop
函数.
对这几个也算不上有很深的理解,只是平时用到些许timer,thread。
想起有次去baidu笔试遇到runloop和timer等的区别,当时就不会。
两三月过去了,如今终于稍微整理了下。
有不对的地方盼指正。
(版权所有哦)
· NSThread:常见的线程
每个进程里都有多个线程,我们一般如下实用thread:
[NSThread detachNewThreadSelector:@selector(myThreadMainMethod:) toTarget:self withObject:nil];
如果函数需要输入参数,那么可以从object传进去。你也可以这样实现
NSThread* myThread = [[NSThread alloc] initWithTarget:self selector:@selector(myThreadMainMethod:) object:nil];
[myThread start]; // Actually create the thread
(from apple: threading PG)
你的对象也可以直接使用线程:
[myObj performSelectorInBackground:@selector(doSomething) withObject:nil];
· NSTimer:定时器
等待一定时间后,触发某个事件发生,可循环触发。默认是添加到当前runloop。你也可以添加到自己新建的runloop里去,注意如果添加的话runloop会retain timer,你应当release timer而将timer交给runloop,就像将operation加入operationQueue中一样。
可以很简单的调用:
[NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(addLabel) userInfo:nil repeats:YES];
- (void)addLabel
{
label.text = [NSString stringWithFormat:@"%d",num++];
}
每隔2秒,就触发定时器,向self发送addLabel消息。
· NSRunLoop
当有输入信号(input source,比如键盘鼠标的操作、),NSPort和NSConnection对象时,runloop提供了一个程序接口,即等待输入。但是我们从来都不需要去创建或者去管理一个runloop。在每个进程中都相应的有runloop,不管时当前的主进程还是你新建的进程。如果需要访问当前进程的runloop可以调用类方法:+ (NSRunLoop *)currentRunLoop。
[[NSRunLoop currentRunLoop] performSelector:@selector(addLabel2)
target:self
argument:nil
order:0
modes:[NSArray arrayWithObjects:@"NSDefaultRunLoopMode",nil]]
//举个例子而已,一般不会这样用
一般需要使用runloop也就是对于netservice,stream等对象以某种模式schedule在当前的runloop,如:
[[_session inputStream] scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];。
Runloop的作用在于当有事情要做时它使当前的thread工作,没有事情做时又使thread 休眠sleep。注意Runloop并不是由系统自动控制的,尤其是对那些你新建的次线程你需要对其进行显示的控制。
Runloop顾名思义就是一个不停的循环,不断的去check输入,如下图。
我们需要了解runloop modes这对判断事件来源以及添加到runloop时很有必要。
正如之前我们所说,只有创建了次线程才需要我们管理runloop,但是也并不是创建了次线程就一定需要管理runloop,仅当:
o Use ports or custom input sources to communicate with other threads.
o Use timers on the thread.
o Use any of the performSelector... methods in a Cocoa application.
o Keep the thread around to perform periodic tasks.
你还可以注册runloop,这样可以使用kvo。
· NSTask:
使用task你可以运行其它程序作为当前程序的子进程,并监控它的运行。它和线程的不同之处在于它并不何创建它的进程(父进程)共享内存。可以说是“完全”独立的一个东西。
Run Loop详解
学习过程中,将Threading PG中的Run Loops翻译了下,权当是做为笔记。原文见 Run Loops。
转载请注明,谢谢。
http://www.cnblogs.com/scorpiozj/
Run loops是线程的基础架构部分。一个run loop就是一个事件处理循环,用来不停的调配工作以及处理输入事件。使用run loop的目的是使你的线程在有工作的时候工作,没有的时候休眠。
Run loop的管理并不完全是自动的。你仍必须设计你的线程代码以在适当的时候启动run loop并正确响应输入事件。Cocoa和CoreFundation都提供了run loop对象方便配置和管理线程的run loop。你创建的程序不需要显示的创建run loop;每个线程,包括程序的主线程(main thread)都有与之相应的run loop对象。但是,自己创建的次线程是需要手动运行run loop的。在carbon和cocoa程序中,程序启动时,主线程会自行创建并运行run loop。
接下来的部分将会详细介绍run loop以及如何为你的程序管理run loop。关于run loop对象可以参阅sdk文档。
解析Run Loop
run loop,顾名思义,就是一个循环,你的线程在这里开始,并运行事件处理程序来响应输入事件。你的代码要有实现循环部分的控制语句,换言之就是要有while或for语句。在run loop中,使用run loop对象来运行事件处理代码:响应接收到的事件,启动已经安装的处理程序。
Run loop处理的输入事件有两种不同的来源:输入源(input source)和定时源(timer source)。输入源传递异步消息,通常来自于其他线程或者程序。定时源则传递同步消息,在特定时间或者一定的时间间隔发生。两种源的处理都使用程序的某一特定处理路径。
图1-1显示了run loop的结构以及各种输入源。输入源传递异步消息给相应的处理程序,并调用runUntilDate:方法退出。定时源则直接传递消息给处理程序,但并不会退出run loop。
图1-1 run loop结构和几种源
除了处理输入源,run loop也会生成关于run loop行为的notification。注册的run-loop 观察者可以收到这些notification,并做相应的处理。可以使用Core Foundation在你的线程注册run-loop观察者。
下面介绍run loop的组成,以及其运行的模式。同时也提及在处理程序中不同时间发送不同的notification。
Run Loop Modes
Run loop模式是所有要监视的输入源和定时源以及要通知的注册观察者的集合。每次运行run loop都会指定其运行在哪个模式下。以后,只有相应的源会被监视并允许接收他们传递的消息。(类似的,只有相应的观察者会收到通知)。其他模式关联的源只有在run loop运行在其模式下才会运行,否则处于暂停状态。
通常代码中通过指定名字来确定模式。Cocoa和core foundation定义了默认的以及一系列常用的模式,都是用字符串来标识。当然你也可以指定字符串来自定义模式。虽然你可以给模式指定任何名字,但是所有的模式内容都是相同的。你必须添加输入源,定时器或者run loop观察者到你定义的模式中。
通过指定模式可以使得run loop在某一阶段只关注感兴趣的源。大多数时候,run loop都是运行在系统定义的默认模式。但是模态面板(modal panel)可以运行在 “模态”模式下。在这种模式下,只有和模态面板相关的源可以传递消息给线程。对于次线程,可以使用自定义模式处理时间优先的操作,即屏蔽优先级低的源传递消息。
Note:模式区分基于事件的源而非事件的种类。例如,你不可以使用模式只选择处理鼠标按下或者键盘事件。你可以使用模式监听端口, 暂停定时器或者其他对源或者run loop观察者的处理,只要他们在当前模式下处于监听状态。
表1-1列出了cocoa和Core Foundation预先定义的模式。
表1-1
输入源
输入源向线程发送异步消息。消息来源取决于输入源的种类:基于端口的输入源和自定义输入源。基于端口的源监听程序相应的端口,而自定义输入源则关注自定义的消息。至于run loop,它不关心输入源的种类。系统会去实现两种源供你使用。两类输入源的区别在于如何显示的:基于端口的源由内核自动发送,而自定义的则需要人工从其他线程发送。
当你创建输入源,你需要将其分配给run loop中的一个或多个模式。模式只会在特定事件影响监听的源。大多数情况下,run loop运行在默认模式下,但是你也可以使其运行在自定义模式。若某一源在当前模式下不被监听,那么任何其生成的消息只有当run loop运行在其关联的模式下才会被传递。
下面讨论这几种输入源。
http://www.cnblogs.com/scorpiozj/
基于端口的源:
cocoa和core foundation为使用端口相关的对象和函数创建的基于端口的源提供了内在支持。Cocoa中你从不需要直接创建输入源。你只需要简单的创建端口对象,并使用NSPort的方法将端口对象加入到run loop。端口对象会处理创建以及配置输入源。
在core foundation,你必须手动的创建端口和源,你都可以使用端口类型(CFMachPortRef,CFMessagePortRef,CFSocketRef)来创建。
更多例子可以看 配置基于端口的源。
自定义输入源:
在Core Foundation程序中,必须使用CFRunLoopSourceRef类型相关的函数来创建自定义输入源,接着使用回调函数来配置输入源。Core Fundation会在恰当的时候调用回调函数,处理输入事件以及清理源。
除了定义如何处理消息,你也必须定义源的消息传递机制——它运行在单独的进程,并负责传递数据给源和通知源处理数据。消息传递机制的定义取决于你,但最好不要过于复杂。
关于创建自定义输入源的例子,见 定义自定义输入源。关于自定义输入源的信息参见CFRunLoopSource。
Cocoa Perform Selector Sources:
除了基于端口的源,Cocoa提供了可以在任一线程执行函数(perform selector)的输入源。和基于端口的源一样,perform selector请求会在目标线程上序列化,减缓许多在单个线程上容易引起的同步问题。而和基于端口的源不同的是,perform selector执行完后会自动清除出run loop。
当perform selector在其它线程中执行时,目标线程须有一活动中的run loop。对于你创建的线程而言,这意味着线程直到你显示的开始run loop否则处于等待状态。然而,由于主线程自己启动run loop,在程序调用applicationDidFinishlaunching:的时候你会遇到线程调用的问题。因为Run loop通过每次循环来处理所有排列的perform selector调用,而不时通过每次的循环迭代来处理perform selector。
表1-2列出了NSObject可以在其它线程使用的perform selector。由于这些方法时定义在NSObject的,你可以在包括POSIX的所有线程中使用只要你有objc对象的访问权。注意这些方法实际上并没有创建新的线程以运行perform selector。
表1-2
定时源
定时源在预设的时间点同步地传递消息。定时器时线程通知自己做某事的一种方法。例如,搜索控件可以使用定时器,当用户连续输入的时间超过一定时间时,就开始一次搜索。这样,用户就可以有足够的时间来输入想要搜索的关键字。
尽管定时器和时间有关,但它并不是实时的。和输入源一样,定时器也是和run loop的运行模式相关联的。如果定时器所在的模式未被run loop监视,那么定时器将不会开始直到run loop运行在相应的模式下。类似的,如果定时器在run loop处理某一事件时开始,定时器会一直等待直到下次run loop开始相应的处理程序。如果run loop不再运行,那定时器也将永远不开始。
你可以选择定时器工作一次还是定时工作。如果定时工作,定时器会基于安排好的时间而非实际时间,自动的开始。举个例子,定时器在某一特定时间开始并设置5秒重复,那么定时器会在那个特定时间后5秒启动,即使在那个特定时间定时器延时启动了。如果定时器延迟到接下来设定的一个会多个5秒,定时器在这些时间段中也只会启动一次,在此之后,正常运行。(假设定时器在时间1,5,9。。。运行,如果最初延迟到7才启动,那还是从9,13,。。。开始)。
Run Loop观察者
源是同步或异步的传递消息,而run loop观察者则是在运行run loop的时候在特定的时候开始。你可以使用run loop观察者来为某一特定事件或是进入休眠的线程做准备。你可以将观察者将以下事件关联:
Run loop入口
Run loop将要开始定时
Run loop将要处理输入源
Run loop将要休眠
Run loop被唤醒但又在执行唤醒事件前
Run loop终止
你可以给cocoa和carbon程序随意添加观察者,但是如果你要定义观察者的话就只能使用core fundation。使用CFRunLoopObserverRed类型来创建观察者实例,它会追踪你自定义的回调函数以及其它你感兴趣的地方。
和定时器类似,观察者可以只用一次或循环使用。若只用一次,那在结束的时候会移除run loop,而循环的观察者则不会。你需要制定观察者是一次/多次使用。
消息的run loop顺序
每次启动,run loop会自动处理之前未处理的消息,并通知观察者。具体的顺序,如下:
通知观察者,run loop启动
通知观察者任何即将要开始的定时器
通知观察者任何非基于端口的源即将启动
启动任何准备好的非基于端口的源
如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9。
通知观察者线程进入休眠
将线程之于休眠直到任一下面的事件发生
某一事件到达基于端口的源
定时器启动
设置了run loop的终止时间
run loop唤醒
通知观察者线程将被唤醒。
处理未处理的事件
如果用户定义的定时器启动,处理定时事件并重启run loop。进入步骤2
如果输入源启动,传递相应的消息
run loop唤醒但未终止,重启。进入步骤2
通知观察者run loop结束。
(标号应该连续,不知道怎么改)
因为观察者的消息传递是在相应的事件发生之前,所以两者之间可能存在误差。如果需要精确时间控制,你可以使用休眠和唤醒通知以此来校对实际发生的事件。
因为定时器和其它周期性事件那是在run loop运行后才启动,撤销run loop也会终止消息传递。典型的例子就是鼠标路径追踪。因为你的代码直接获取到消息而不是经由程序传递,从而不会在实际的时间开始而须使得鼠标追踪结束并将控制权交给程序后才行。
使用run loop对象可以唤醒Run loop。其它消息也可以唤醒run loop。例如,添加新的非基于端口的源到run loop从而可以立即执行输入源而不是等待其他事件发生后再执行。
何时使用Run Loop
http://www.cnblogs.com/scorpiozj/archive/2011/05/26/2058167.html
只有在为你的程序创建次线程的时候,才需要运行run loop。对于程序的主线程而言,run loop是关键部分。Cocoa和carbon程序提供了运行主线程run loop的代码同时也会自动运行run loop。IOS程序UIApplication中的run方法在程序正常启动的时候就会启动run loop。同样的这部分工作在carbon程序中由RunApplicationEventLoop负责。如果你使用xcode提供的模板创建的程序,那你永远不需要自己去启动run loop。
而对于次线程,你需要判断是否需要run loop。如果需要run loop,那么你要负责配置run loop并启动。你不需要在任何情况下都去启动run loop。比如,你使用线程去处理一个预先定义好的耗时极长的任务时,你就可以毋需启动run loop。Run loop只在你要和线程有交互时才需要,比如以下情况:
使用端口或自定义输入源和其他线程通信
使用定时器
cocoa中使用任何performSelector
使线程履行周期性任务
如果决定在程序中使用run loop,那么配置和启动都需要自己完成。和所有线程编程一样,你需要计划好何时退出线程。在退出前结束线程往往是比被强制关闭好的选择。详细的配置和推出run loop的信息见 使用run loop对象。
使用Run loop对象
run loop对象提供了添加输入源,定时器和观察者以及启动run loop的接口。每个线程都有唯一的与之关联的run loop对象。在cocoa中,是NSRunLoop对象;而在carbon或BSD程序中则是指向CFRunLoopRef类型的指针。
获得run loop对象
获得当前线程的run loop,可以采用:
cocoa:使用NSRunLoop的currentRunLoop类方法
使用CFRunLoopGetCurrent函数
虽然CFRunLoopRef类型和NSRunLoop对象并不完全等价,你还是可以从NSRunLoop对象中获取CFRunLoopRef类型。你可以使用NSRunLoop的getCFRunLoop方法,返回CFRunLoopRef类型到Core Fundation中。因为两者都指向同一个run loop,你可以任一替换使用。
配置run loop
在次线程启动run loop前,你必须至少添加一类源。因为如果run loop没有任何源需要监视的话,它会在你启动之际立马退出。
此外,你也可以添加run loop观察者来监视run loop的不同执行阶段。首先你可以创建CFRunLoopObserverRef类型并使用CFRunLoopAddObserver将它添加金run loop。注意即使是cocoa程序,run loop观察者也需要由core foundation函数创建。
以下代码3-1实现了添加观察者进run loop,代码简单的建立了一个观察者来监视run loop的所有活动,并将run loop的活动打印出来。
Creating a run loop observer
- (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);
}
如果线程运行事件长,最好添加一个输入源到run loop以接收消息。虽然你可以使用定时器,但是定时器一旦启动后当它失效时也会使得run loop退出。虽然定时器可以循环使得run loop运行相对较长的时间,但是也会导致周期性的唤醒线程。与之相反,输入源会等待某事件发生,于是线程只有当事件发生后才会从休眠状态唤醒。
启动run loop
run loop只对程序的次线程有意义,并且必须添加了一类源。如果没有,在启动后就会退出。有几种启动的方法,如:
无条件的
预设的时间
特定的模式
无条件的进入run loop是最简单的选择,但也最不提倡。因为这样会使你的线程处在一个永久的run loop中,这样的话你对run loop本身的控制就会很小。你可以添加或移除源,定时器,但是只能通过杀死进程的办法来退出run loop。并且这样的run loop也没有办法运行在自定义模式下。
用预设时间来运行run loop是一个比较好的选择,这样run loop在某一事件发生或预设的事件过期时启动。如果是事件发生,消息会被传递给相应的处理程序然后run loop退出。你可以重新启动run loop以处理下一个事件。如果是时间过期,你只需重启run loop或使用定时器做任何的其他工作。**
此外,使run loop运行在特定模式也是一个比较好的选择。模式和预设时间不是互斥的,他们可以同时存在。模式对源的限制在run loop模式部分有详细说明。
Listing3-2代码描述了线程的整个结构。代码的关键是说明了run loop的基本结构。必要时,你可以添加自己的输入源或定时器,然后重复的启动run loop。每次run loop返回,你要检查是否有使线程退出的条件发生。代码中使用了Core Foundation的run loop程序,这样就能检查返回结果从而判断是否要退出。若是cocoa程序,也不需要关心返回值,你也可以使用NSRunLoop的方法运行run loop(代码见listing3-14)
Listing 3-2 Running a run loop
- (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使用的模式。
退出run loop
在run loop处理事件前,有两种方法使其退出:
设置超时限定
通知run loop停止
如果可以配置的话,使用第一种方法是较好的选择。这样,可以使run loop完成所有正常操作,包括发送消息给run loop观察者,最后再退出。
使用CFRunLoopStop来停止run loop也有类似的效果。Run loop也会把所有未发送的消息发送完后再退出。与设置时间的区别在于你可以在任何情况下停止run loop。
尽管移除run loop的输入源和定时器也可以使run loop退出,但这并不是可靠的退出run loop的办法。一些系统程序会添加输入源来处理必须的事件。而你的代码未必会考虑到这些,这样就没有办法从系统程序中移除,从而就无法退出run loop。
线程安全和run loop对象
线程是否安全取决于你使用哪种API操纵run loop。Core Foundation中的函数通常是线程安全的可以被任意线程调用。但是,如果你改变了run loop的配置然后需要进行某些操作,你最好还是在run loop所在线程去处理。如果可能的话,这样是个好习惯。
至于Cocoa的NSRunLoop则不像Core Foundation具有与生俱来的线程安全性。你应该只在run loop所在线程改变run loop。如果添加yuan或定时器到属于另一个线程的run loop,程序会崩溃或发生意想不到的错误。
Run loop 源的配置
下面的例子说明了如果使用cocoa和core foundation来建立不同类型的输入源。
定义自定义输入源
遵循下列步骤来创建自定义的输入源:
输入源要处理的信息
使感兴趣的客户知道如何和输入源交互的调度程序
处理客户发送请求的程序
使输入源失效的取消程序
由于你自己创建源来处理消息,实际配置设计得足够灵活。调度,处理和取消程序是你创建你得自定义输入源时总会需要用到得关键程序。但是,输入源其他的大部分行为都是由其他程序来处理。例如,由你决定数据传输到输入源的机制,还有输入源和其他线程的通信机制。
图3-2列举了自定义输入源的配置。在这个例子中,程序的主线程保持了输入源,输入源所需的命令缓冲区和输入源所在的run loop的引用。当主线程有任务,需要分发给目标线程,主线程会给命令缓冲区发送命令和必须的信息,这样活动线程就可以开始执行任务。(因为主线程和输入源所在线程都须访问命令缓冲区,所以他们的操作要注意同步。)一旦命令传送了,主线程会通知输入源并且唤醒活动线程的run loop。而一收到唤醒命令,run loop会调用输入源的处理部分,由它来执行命令缓冲区中相应的命令。
图3-2
下面解释下上图的关键代码。
定义输入源
定义输入源需要使用Core Foundation来配置run loop源并把它添加到run loop。基本的函数是C函数,当然你也可以用objc或C++来封装操作。
图3-2中的输入源使用了objc对象来管理命令缓冲区和run loop。Listing3-3说明了此对象的定义:RunLoopSource对象管理着命令缓冲区并以此来接收其他线程的消息;RunLoopContext对象是一个用于传递RunLoopSource对象和run loop引用给程序主线程的一个容器。
Listing 3-3 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.
@interface RunLoopContext : NSObject
{
CFRunLoopRef runLoop;
RunLoopSource* source;
}
@property (readonly) CFRunLoopRef runLoop;
@property (readonly) RunLoopSource* source;
- (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop;
@end
虽然输入源的数据定义是objc代码,但是将源添加进run loop却需要c的回调函数。上述函数在像Listing3-4一样,在添加时调用。因为这个输入源只有一个客户(即主线程),它使用调度函数发送注册信息给程序的代理(delegate)。当代理需要和输入源通信时,就可以使用RunLoopContext对象实现。
Listing 3-4 Scheduling a run loop source
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];
}
一个重要的回调函数就是用来处理自定义数据。Lising3-5说明了如何调用这个回调函数。这里只是简单的将请求传递到sourceFired方法,然后继续处理在命令缓存区的命令。
Listing 3-5 Performing work in the input source
void RunLoopSourcePerformRoutine (void *info)
{
RunLoopSource* obj = (RunLoopSource*)info;
[obj sourceFired];
}
使用CFRunLoopSourceInvalidate函数移除输入源,系统会调用输入源的取消程序。你可以以此通知客户输入源不再有效,客户可以释放输入源的引用。Listing3-6说明了取消回调函数的调用,其中给另一个RunLoopContext对象发送了程序代理,通知代理去除源的引用。
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];
}
安装输入源到run loop
Listing3-7说明了RunLoopSource的init和addToCurrentRunLoop函数。Init函数创建CFRunLoopSourceRef类型,传递RunLoopSource对象做为信息这样回调函数持有对象的引用。输入源的安装当工作线程运行addToCurrentRunLoop方法,然后调用RunLoopSourceScheduledRoutine回调函数。一旦源被添加,线程就运行run loop监听事件。
Listing 3-7 Installing the run loop source
- (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);
}
统筹输入源的客户
为了使添加的输入源有用,你需要处理源以及从其他线程发送信号。输入源的主要工作就是将与源关联的线程休眠,直到有事件发生。这就意味着程序中的线程必须知道输入源信息并有办法与之 通信。
通知客户关于输入源信息的方法之一就是当输入源安装完成后发送注册请求。你可以注册输入源到你需要的客户,或者通过注册中间代理由代理将输入源到感兴趣的客户。Listing3-8说明了程序代理定义的注册方法以及它在RUnLoopSource对象的调度函数调用时如何运行。函数接收RUnLoopSource提供的RunLoopContext对象,然后将其胶乳源队列。另外,也说明了源移除run loop时候的取消注册方法。
Listing 3-8 Registering and removing an input source with the application delegate
- (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];
}
通知输入源
客户发送数据到输入源后,必须发信号通知源并且唤醒run loop。发信好意味着让run loop明白源已经做好处理消息的准备。因为信号发生的时候线程可能休眠着,你必须自己唤醒run loop。如果不这样做的话会导致延迟处理消息。
Listing3-9说明了RunLoopSource对象的fireCommandsOnRunLoop方法。客户如果准备好处理加入缓冲区的命令后会运行此方法。
Listing 3-9 Waking up the run loop
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
CFRunLoopSourceSignal(runLoopSource);
CFRunLoopWakeUp(runloop);
}
配置定时源
创建定时源你所需要做的就是创建定时器并加入run loop调度。Cocoa程序中使用NSTimer类而Core Foundation中使用CFRunLoopTimerRef类型。本质上,NSTimer类是Core Foundation的简单扩展,这样就提供了便利的特征,例如都能使用相同的函数创建和调配定时器。
Cocoa中可以使用以下函数创建并调配:
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
scheduledTimerWithTimeInterval:invocation:repeats:
上述方法创建了定时器并使之以默认模式添加到当前线程的run loop。你可以自己调度定时器如果你选择自己创建定时器并使用addTimer:forMode:方法添加到run loop。两种方法都做了相同的事,区别在于你对定时器的控制权。如果你自己创建的话,你可以选择添加的模式。Listing3-10说明了如果这样做。第一个定时器在初始化后1秒开始可以后每隔0.1秒运行,第二个定时器则在初始化后0。2秒开始以后每隔0。2运行。
Listing 3-10 Creating and scheduling timers using NSTimer
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];
Listing3-11是使用Core Foundation函数配置定时器的代码。这个例子中文本结构例没有任何用户定义的信息,但是你可以使用这个结构体传递任何你想传递的信息给定时器。关于结构体的详细信息,参见CFRunLoopTimer。
Listing 3-11 Creating and scheduling a timer using Core Foundation
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);
配置基于端口的输入源
cocoa和core foundation都提供了基于端口的对象用于线程或进程间的通信。下面的部分说明了使用几种不同的端口对象建立端口通信。
配置NSMachPort对象
建立和NSMachPort对象的本地连接,你需要创建端口对象并将之加入主线程的run loop。当运行次线程的时候,你传递端口对象到线程的入口点。次线程可以使用相同的端口对象将消息返回给主线程。