(5)iPhone开发基础 - 消息队列

时间:2022-09-27 23:20:21

在图形化操作系统出来之前都是基于控制台的应用程序,往往在执行完成之后自动退出, 如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, 我将程序在响应鼠标单击按钮时的调用栈显示如下:

(5)iPhone开发基础 - 消息队列


了解了NSRunLoop的作用后,我们再来看一下它的应用范围:

(5)iPhone开发基础 - 消息队列

由上图我们可知,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. 自定义源