在图形化操作系统出来之前都是基于控制台的应用程序,往往在执行完成之后自动退出, 如ps -A 显示系统的所有进程,而我们的iphone或窗口应用程序都是基于图形界面的软件,为了界面不至于马上消失,我们需要让程序不停的运行,并绘制图形界面,类似于下面的伪代码:
int main() { while (要求退出) { 响应各种消息 } return 0; }
这就是我们消息队列的原型,系统的启动的时候创建一个线程,然后等待该线程结束,在等待的过程中响应各种消息,如鼠标,键盘等。 这里所创建的线程就是程序的主线程,它自动的创建一个消息队列,然后等待它完成。这里的消息队列就是RunLoop, 我们查阅Foundation会发现有两个相关的对象NSRunLoop和CFRunLoop, 其实这两个东西是一样的,NSRunLoop主要是用于objective-c程序,而CFRunLoop主要用于C/C++程序,这是因为C/C++程序无法使用objective-c对象而创建的一个类。
注意: 所有线程都自动创建一个RunLoop, 在线程内通过 [NSRunLoop currentRunLoop] 获得当前线程的RunLoop.
为了证明它确实是使用的RunLoop, 我将程序在响应鼠标单击按钮时的调用栈显示如下:
了解了NSRunLoop的作用后,我们再来看一下它的应用范围:
由上图我们可知,NSRunLoop响应两种类型的消息: Input sources 和 Timer sources. 就是前面我们讲到的,它在等待响应消息时,只处理这两种消息源。
为了更好的理解RunLoop, 我将以伪码的形式来说明它内部的运行原理:
1. 启动函数 run
我们先来看一段伪代码:
- (void)run { while([self hasSourcesOrTimers]) [self runMode: NSDefaultRunLoopMode beforeDate: [NSDate distantFuture]]; }我们知道了NSRunLoop在主线程中是自动启动的,也就是调用run函数,这个函数首先检查是否有输入源(input sources)和时间源(timer sources), 如果没有,直接返回,否则不停的运行runMode, 直到所有源全部处理完毕。
2. 启动函数 runUntilDate
我们还是以伪代码来描述:
- (void)runUntilDate: (NSDate *)limitDate { while([self hasSourcesOrTimers]) { [self runMode: NSDefaultRunLoopMode beforeDate: limitDate]; // check limitDate at the end of the loop to ensure that // the runloop always runs at least once if([limitDate timeIntervalSinceNow] < 0) break; } }同上面一样,如果没有任何源则直接退出,否则不停的运行runMode 直到所有源全部处理完毕,或到达指定的时间,这两个条件的任何一个条件满足则退出。
3. 执行函数: runMode
下面我们来看一下runMode, 我们知道Mac OS X是基于unix的操作系统,即所设备都是文件(如鼠标,键盘等,不懂的可以查阅一下资料),所以这里我们用FD来模拟这个函数:
- (BOOL)runMode: (NSString *)mode beforeDate: (NSDate *)limitDate { if(![self hasSourcesOrTimersForMode: mode]) return NO; // 为了对timer的支持,我们在这里设置一个标签, BOOL didFireInputSource = NO; while(!didFireInputSource) { // 创建一个空的设备描述FD fd_set fdset; FD_ZERO(&fdset); for(inputSource in [_inputSources objectForKey: mode]) FD_SET([inputSource fileDescriptor], &fdset); // 我们这里假设已经设置了limitDate NSTimeInterval timeout = [limitDate timeIntervalSinceNow]; // 这里计算timer源里的最短timeout for(timer in [_timerSources objectForKey: mode]) timeout = MIN(timeout, [[timer fireDate] timeIntervalSinceNow]); // select等待某一设置准备完成 select(fdset, timeout); // 首先检查输入源 for(inputSource in [[[_inputSources objectForKey: mode] copy] autorelease]) if(FD_ISSET([inputSource fileDescrptor], &fdset)) { didFireInputSource = YES; [inputSource fileDescriptorIsReady]; } // 更新timer for(timer in [[[_timerSources objectForKey: mode] copy] autorelease]) if([[timer fireDate] timeIntervalSinceNow] <= 0) [timer fire]; // 是否timeout, 一但timeout直接退出 if([limitDate timeIntervalSinceNow] < 0) break; } return YES; }由此我们可以看出: 输入源和时间源的检查并不是总在运行的,所以,我们在run的时候,需要用while语句,直到运行完毕。
下面我们进入RunLoop的实际使用:
1. RunLoop的模式
下图是RunLoop启动时所使用的模式,以及说明:
模式 | 名称 | 描述 |
Default | NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) |
缺省情况下,将包含所有操作,并且大多数情况下都会使用此模式 |
Connection | NSConnectionReplyMode (Cocoa) | 此模式用于处理NSConnection的回复事件 |
Modal | NSModalPanelRunLoopMode (Cocoa) | 模态模式,此模式下,RunLoop只对处理模态相关事件 |
Event Tracking | NSEventTrackingRunLoopMode (Cocoa) | 此模式下用于处理窗口事件,鼠标事件等 |
Common Modes | NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation) |
此模式用于配置组模式,一个输入源与此模式关联,则输入源与组中的所有模式相关联,用户可以自定义模式。 |
2. 输入源
输入源分为三种: 1) NSPort源 2) 自定义源 3) 定时源
3. RunLoop观察者
如果大家不熟悉设计模式,可以找本设计模式方面的书看一下,这里的观察者就是使用的观察者模式,简单的说明一下,就是如果我是你的观察者,那在某些事件发生时,你会主动通知我,这里的事件包括:
- Run loop入口
- Run loop将要开始定时
- Run loop将要处理输入源
- Run loop将要休眠
- Run loop被唤醒但又在执行唤醒事件前
- Run loop终止
就是在以上这些事件产生的时候,会通知所有与之关联的观察者对象。
下面我们来看一个例子
HelloRunLoop.h
#import "CoreHeader.h" @interface HelloRunloop : NSObject { volatile BOOL propTest0; NSString* propTest1; } - (void) run:(id)arg; - (void) observerRunLoop; - (void) wakeUpMainThreadRunloop:(id)arg; - (IBAction)start:(id)sender; @end
HelloRunLoop.h
#import "HelloRunloop.h" void myRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { switch (activity) { case kCFRunLoopEntry: NSLog(@"run loop entry"); break; case kCFRunLoopBeforeTimers: NSLog(@"run loop before timers"); break; case kCFRunLoopBeforeSources: NSLog(@"run loop before sources"); break; case kCFRunLoopBeforeWaiting: NSLog(@"run loop before waiting"); break; case kCFRunLoopAfterWaiting: NSLog(@"run loop after waiting"); break; case kCFRunLoopExit: NSLog(@"run loop exit"); break; default: break; } } @implementation HelloRunloop - (void) run:(id)arg { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; sleep(5); propTest0 = NO; [self performSelectorOnMainThread:@selector(wakeUpMainThreadRunloop:) withObject:nil waitUntilDone:NO]; [pool release]; } - (void)observerRunLoop { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSRunLoop *myRunLoop = [NSRunLoop currentRunLoop]; CFRunLoopObserverContext context = {0, self, NULL, NULL, NULL}; CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context); if (observer) { CFRunLoopRef cfRunLoop = [myRunLoop getCFRunLoop]; CFRunLoopAddObserver(cfRunLoop, observer, kCFRunLoopDefaultMode); } [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(doFireTimer:) userInfo:nil repeats:YES]; NSInteger loopCount = 10; do { [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; loopCount--; } while (loopCount); [pool release]; } - (void) wakeUpMainThreadRunloop:(id)arg { NSLog(@"wakeup main thread runloop."); } - (IBAction)start:(id)sender { propTest0 = YES; //[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil]; [NSThread detachNewThreadSelector:@selector(observerRunLoop:) toTarget:self withObject:nil]; propTest1 = @"waiting"; while (propTest0) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } propTest1 = @"end"; } @end
上面有两段不同的代码来确保RunLoop处理了输入源:
do { [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; loopCount--; } while (loopCount);
和
while (propTest0) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; }
下面我们再介绍一种在Core Foundation下的代码:
BOOL done = NO; do { SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES); if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished)) done = YES; } while (!done);
用while(true)是不提倡的做法,这样只能杀掉线程RunLoop才会停止。
下面我们来讨论一下RunLoop的三种输入源:
1. NSPort源
我们在上一章讲过,NSPort源有3种类型:NSMachPort, NSMessagePort 和 NSSocketPort, 而NSMessagePort已经不被推荐使用, 在iOS 5中,NSMessagePort只是一个空的对象了,所以我们只会讲解NSMachPort 和 NSSocketPort, 下面我们讲解这两种输入源:
1) NSMachPort输入源
HelloPortRunLoop.h
#import <Foundation/Foundation.h> @interface MyWorkerClass : NSObject <NSMachPortDelegate> { } + (void)LaunchThreadWithPort:(id)inData; - (void)sendCheckinMessage:(NSPort*)outPort; - (BOOL) shouldExit; @end @interface HelloPortRunLoop : NSObject<NSMachPortDelegate> { } - (void) launchThread; @end
HelloPortRunLoop.m
#import "HelloPortRunLoop.h" //#import <Foundation/NSPortMessage.h> #define kCheckinMessage 100 @implementation MyWorkerClass - (BOOL) shouldExit { return YES; } +(void)LaunchThreadWithPort:(id)inData { NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; NSPort* distantPort = (NSPort*)inData; MyWorkerClass* workerObj = [[self alloc] init]; [workerObj sendCheckinMessage:distantPort]; [distantPort release]; do { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } while (![workerObj shouldExit]); [workerObj release]; [pool release]; } - (void)handleMachMessage:(void *)msg { NSLog(@"MyWorkerClass: handle mach message"); } - (void)sendCheckinMessage:(NSPort*)outPort { //[self setRemotePort:outPort]; NSPort* myPort = [NSMachPort port]; [myPort setDelegate:self]; [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode]; [outPort sendBeforeDate:[NSDate distantFuture] msgid:kCheckinMessage components:nil from:myPort reserved:0]; /* NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:outPort receivePort:myPort components:nil]; if (messageObj) { [messageObj setMsgId:kCheckinMessage]; [messageObj sendBeforeDate:[NSDate date]]; } */ } @end @implementation HelloPortRunLoop - (void)handleMachMessage:(void *)msg { NSLog(@"HelloPortRunLoop:handle mach message"); } /* - (void)handlePortMessage:(NSPortMessage*)portMessage { uint32_t message = [portMessage msgid]; NSPort* distantPort = nil; if (message == kCheckinMessage) { distantPort = [portMessage sendPort]; [self storeDistantPort:distantPort]; } else { // Handle other messages. } }*/ - (void) launchThread { NSPort* myPort = [NSMachPort port]; if (myPort) { [myPort setDelegate:self]; [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode]; [NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:) toTarget:[MyWorkerClass class] withObject:myPort]; } } @end上面的代码中注释掉的代码在xOS中可以运行,但iOS中已经取消对NSMessagePort的支持,所以无法运行。
创建与执行代码:
HelloPortRunLoop* hpr = [[HelloPortRunLoop alloc] init]; [hpr launchThread]
程序运行过程如下:
a. 在主线程中创建次线程, [lpr launchThread] 函数负责创建次线程: LaunchThreadWithPort:
b. 次线程给主线程发送check in消息: sendCheckinMessage.
c. 主线程获取消息: - (void)handleMachMessage:(void *)msg
2. NSSocketPort
在上一章讲过NSConnection会自动将输入源加入到RunLoop中,NSSocketPort的操作是非透明的,具体应用请参看上一章《分布式对象》.
3. 自定义源