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

在图形化操作系统出来之前都是基于控制台的应用程序,往往在执行完成之后自动退出, 如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启动时所使用的模式,以及说明:

模式名称描述
DefaultNSDefaultRunLoopMode (Cocoa)
kCFRunLoopDefaultMode (Core Foundation)
缺省情况下,将包含所有操作,并且大多数情况下都会使用此模式
ConnectionNSConnectionReplyMode (Cocoa)此模式用于处理NSConnection的回复事件
ModalNSModalPanelRunLoopMode (Cocoa)模态模式,此模式下,RunLoop只对处理模态相关事件
Event TrackingNSEventTrackingRunLoopMode (Cocoa)此模式下用于处理窗口事件,鼠标事件等
Common ModesNSRunLoopCommonModes (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. 自定义源













  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值