在图形化操作系统出来之前都是基于控制台的应用程序,往往在执行完成之后自动退出, 如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, 我将程序在响应鼠标单击按钮时的调用栈显示如下:
![iPhone开发基础 - 消息队列_第1张图片](http://img.e-com-net.com/image/info5/8f7dcc10838b43919e150c6423a0fd40.jpg)
了解了NSRunLoop的作用后,我们再来看一下它的应用范围:
![iPhone开发基础 - 消息队列_第2张图片](http://img.e-com-net.com/image/info5/dec7d2db06ba49af83952c59626745a8.jpg)
由上图我们可知,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];
-
-
-
- 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;
-
-
- BOOL didFireInputSource = NO;
- while(!didFireInputSource)
- {
-
- fd_set fdset;
- FD_ZERO(&fdset);
-
- for(inputSource in [_inputSources objectForKey: mode])
- FD_SET([inputSource fileDescriptor], &fdset);
-
-
- NSTimeInterval timeout = [limitDate timeIntervalSinceNow];
-
-
- for(timer in [_timerSources objectForKey: mode])
- timeout = MIN(timeout, [[timer fireDate] timeIntervalSinceNow]);
-
-
- select(fdset, timeout);
-
-
- for(inputSource in [[[_inputSources objectForKey: mode] copy] autorelease])
- if(FD_ISSET([inputSource fileDescrptor], &fdset))
- {
- didFireInputSource = YES;
- [inputSource fileDescriptorIsReady];
- }
-
-
- for(timer in [[[_timerSources objectForKey: mode] copy] autorelease])
- if([[timer fireDate] timeIntervalSinceNow] <= 0)
- [timer fire];
-
-
- 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观察者
如果大家不熟悉设计模式,可以找本设计模式方面的书看一下,这里的观察者就是使用的观察者模式,简单的说明一下,就是如果我是你的观察者,那在某些事件发生时,你会主动通知我,这里的事件包括:
就是在以上这些事件产生的时候,会通知所有与之关联的观察者对象。
下面我们来看一个例子
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(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"
-
-
- #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
- {
-
- 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];
-
-
-
-
-
-
-
-
-
-
- }
-
- @end
-
- @implementation HelloPortRunLoop
-
- - (void)handleMachMessage:(void *)msg
- {
- NSLog(@"HelloPortRunLoop:handle mach message");
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - (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. 自定义源