NSRunLoopiOS_ NSRunLoop

NSRunloop简单细说(十)—— 几个重要的问题(四)

2017-08-24  本文已影响64人  刀客传奇

版本记录

版本号 时间
V1.0 2017.08.24

前言

NSRunloopOC Foundation框架中非常重要的一个类,很多时候我们会使用它,但是未必对其有深入的了解,接下来几篇我就会带着大家重新学习一下NSRunloop这个类,从简单到复杂,从基本到深化,我会一步步的走完。希望对大家有所帮助。感兴趣的可以看我上一篇。
1. NSRunloop简单细说(一)—— 整体了解
2. NSRunloop简单细说(二)—— 获取运行循环及其模式
3. NSRunloop简单细说(三)—— 定时器和端口
4. NSRunloop简单细说(四)—— 开启Runloop
5. NSRunloop简单细说(五)—— 调度和取消消息
6. NSRunloop简单细说(六)—— 几种循环模式详细解析
7. NSRunloop简单细说(七)—— 几个重要的问题(一)
8. NSRunloop简单细说(八)—— 几个重要的问题(二)
9. NSRunloop简单细说(九)—— 几个重要的问题(三)

一、Configuring Timer Sources - 配置定时源

要创建计时器源,您只需创建一个计时器对象并在运行循环中调度它。 在Cocoa中,您可以使用NSTimer类来创建新的定时器对象,而在Core Foundation中,您可以使用CFRunLoopTimerRef opaque类型。 在内部,NSTimer类只是Core Foundation的扩展,它提供了一些方便的功能,如使用相同方法创建和计划定时器的能力。

在Cocoa中,您可以使用以下任一类方法一次创建和调度计时器:

这些方法创建定时器,并以默认模式(NSDefaultRunLoopMode)将其添加到当前线程的运行循环中。 你也可以手动调度计时器,如果您想通过创建NSTimer对象然后使用NSRunLoopaddTimer:forMode:方法将其添加到运行循环中。 这两种技术基本上都是一样的,但是给你不同程度的控制定时器的配置。 例如,如果创建定时器并手动将其添加到运行循环中,则可以使用除默认模式之外的模式来执行此操作。 下面代码显示了如何使用这两种技术创建定时器。 第一个定时器的初始延迟为1秒,但随后每0.1秒钟定时触发。 第二个定时器在初始0.2秒延迟之后开始触发,然后每0.2秒触发一次。

//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];

下面代码展示了使用Core Foundation功能配置定时器所需的代码。 虽然此示例不会在上下文结构中传递任何用户定义的信息,但您可以使用此结构传递定时器所需的任何自定义数据。

//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);

二、Configuring a Port-Based Input Source - 配置基于Port的输入源

CocoaCore Foundation都提供基于端口的对象,用于线程之间或进程之间的通信。 以下部分将介绍如何使用几种不同类型的端口设置端口通信。

1. Configuring an NSMachPort Object - NSMachPort对象的配置

要建立与NSMachPort对象的本地连接,您将创建端口对象并将其添加到主线程的运行循环中。 启动次要线程时,将相同的对象传递给线程的入口点函数。 辅助线程可以使用相同的对象将消息发送回主线程。

Implementing the Main Thread Code

下面代码显示了启动辅助工作线程的主线程代码。 因为Cocoa框架执行了许多用于配置端口和运行循环的介入步骤,所以launchThread方法明显短于其Core Foundation中相应的方法;然而,两者的行为几乎相同。 一个区别是,该方法不是将本地端口的名称发送给工作线程,而是直接发送NSPort对象。

//Main thread launch method

- (void)launchThread
{
    NSPort* myPort = [NSMachPort port];
    if (myPort)
    {
        // This class handles incoming port messages.
        [myPort setDelegate:self];
 
        // Install the port as an input source on the current run loop.
        [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
 
        // Detach the thread. Let the worker release the port.
        [NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
               toTarget:[MyWorkerClass class] withObject:myPort];
    }
}

为了在线程之间建立一个双向通信通道,您可能希望工作线程在登录消息中将自己的本地端口发送到主线程。 接收登录消息让您的主线程知道在启动第二个线程时一切顺利,并且还可以向您发送更多消息到该线程。

下面代码显示了主线程的 handlePortMessage:方法。 当数据到达线程自己的本地端口时调用此方法。 当一个登录消息到达时,该方法直接从端口消息中检索次要线程的端口,并保存以备以后使用。

// Handling Mach port messages

#define kCheckinMessage 100

// Handle responses from the worker thread.

- (void)handlePortMessage:(NSPortMessage *)portMessage

{
    unsigned int message = [portMessage msgid];
    NSPort* distantPort = nil;
    if (message == kCheckinMessage)
    {
      // Get the worker thread’s communications port.
      distantPort = [portMessage sendPort];
      // Retain and save the worker port for later use.
      [self storeDistantPort:distantPort];
    }
    else
    {
      // Handle other messages.
    }
}

Implementing the Secondary Thread Code

对于辅助工作线程,您必须配置线程并使用指定的端口将信息传回主线程。

下面代码显示了设置工作线程的代码。 为线程创建自动释放池后,该方法将创建一个工作对象来驱动线程执行。 工作对象的sendCheckinMessage:方法(如下面第二端代码所示)为工作线程创建一个本地端口,并将一个签入消息发送回主线程。

//Launching the worker thread using Mach ports

+(void)LaunchThreadWithPort:(id)inData
{
    NSAutoreleasePool*  pool = [[NSAutoreleasePool alloc] init];
 
    // Set up the connection between this thread and the main thread.
    NSPort* distantPort = (NSPort*)inData;
 
    MyWorkerClass*  workerObj = [[self alloc] init];
    [workerObj sendCheckinMessage:distantPort];
    [distantPort release];
 
    // Let the run loop process things.
    do
    {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                            beforeDate:[NSDate distantFuture]];
    }
    while (![workerObj shouldExit]);
 
    [workerObj release];
    [pool release];
}

当使用NSMachPort时,本地和远程线程可以使用相同的端口对象进行线程之间的单向通信。 换句话说,由一个线程创建的本地端口对象将成为另一个线程的远程端口对象。

下面代码中显示了次要线程的签入例程。 该方法设置自己的本地端口用于将来的通信,然后发送一个检入消息回主线程。 该方法使用在LaunchThreadWithPort:方法中接收的端口对象作为消息的目标。

 // Sending the check-in message using Mach ports
// Worker thread check-in method

- (void)sendCheckinMessage:(NSPort*)outPort
{
    // Retain and save the remote port for future use.
    [self setRemotePort:outPort];

    // Create and configure the worker thread port.
    NSPort* myPort = [NSMachPort port];
    [myPort setDelegate:self];
    [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];

    // Create the check-in message.
    NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:outPort receivePort:myPort components:nil];
    if (messageObj)
    {
      // Finish configuring the message and send it immediately.
      [messageObj setMsgId:setMsgid:kCheckinMessage];
      [messageObj sendBeforeDate:[NSDate date]];
    }
}

2. Configuring an NSMessagePort Object - NSMessagePort对象的配置

要建立与NSMessagePort对象的本地连接,您不能简单地在线程之间传递端口对象。 远程消息端口必须以名称获取。 在Cocoa中可能需要使用特定的名称注册本地端口,然后将该名称传递给远程线程,以便它可以获取适当的端口对象进行通信。 下面代码显示了要使用消息端口的端口创建和注册过程。

//Registering a message port

NSPort* localPort = [[NSMessagePort alloc] init];
 
// Configure the object and add it to the current run loop.
[localPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode];
 
// Register the port using a specific name. The name must be unique.
NSString* localPortName = [NSString stringWithFormat:@"MyPortName"];
[[NSMessagePortNameServer sharedInstance] registerPort:localPort name:localPortName];

3. Configuring a Port-Based Input Source in Core Foundation - 在Core Foundation中配置基于端口的输入源

本节介绍如何使用Core Foundation在应用程序的主线程和工作线程之间设置双向通信通道。

下面代码显示了应用程序主线程调用的代码,以启动工作线程。 代码的第一件事是设置一个CFMessagePortRef opaque类型来监听来自工作线程的消息。 工作线程需要端口名称进行连接,以便将字符串值传递给工作线程的入口点函数。 端口名称通常在当前用户上下文中是唯一的; 否则,您可能会遇到冲突。

//Attaching a Core Foundation message port to a new 
thread

#define kThreadStackSize        (8 *4096)
 
OSStatus MySpawnThread()
{
    // Create a local port for receiving responses.
    CFStringRef myPortName;
    CFMessagePortRef myPort;
    CFRunLoopSourceRef rlSource;
    CFMessagePortContext context = {0, NULL, NULL, NULL, NULL};
    Boolean shouldFreeInfo;
 
    // Create a string with the port name.
    myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.myapp.MainThread"));
 
    // Create the port.
    myPort = CFMessagePortCreateLocal(NULL,
                myPortName,
                &MainThreadResponseHandler,
                &context,
                &shouldFreeInfo);
 
    if (myPort != NULL)
    {
        // The port was successfully created.
        // Now create a run loop source for it.
        rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
 
        if (rlSource)
        {
            // Add the source to the current run loop.
            CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
 
            // Once installed, these can be freed.
            CFRelease(myPort);
            CFRelease(rlSource);
        }
    }
 
    // Create the thread and continue processing.
    MPTaskID        taskID;
    return(MPCreateTask(&ServerThreadEntryPoint,
                    (void*)myPortName,
                    kThreadStackSize,
                    NULL,
                    NULL,
                    NULL,
                    0,
                    &taskID));
}

在安装端口并启动线程的情况下,主线程可以在等待线程检入时继续其正常执行。当检入消息到达时,它将被分派到主线程的MainThreadResponseHandler函数,下面代码显示的是此函数提取工作线程的端口名称,并创建未来通信的管道。

//Receiving the checkin message

#define kCheckinMessage 100
 
// Main thread port message handler
CFDataRef MainThreadResponseHandler(CFMessagePortRef local,
                    SInt32 msgid,
                    CFDataRef data,
                    void* info)
{
    if (msgid == kCheckinMessage)
    {
        CFMessagePortRef messagePort;
        CFStringRef threadPortName;
        CFIndex bufferLength = CFDataGetLength(data);
        UInt8* buffer = CFAllocatorAllocate(NULL, bufferLength, 0);
 
        CFDataGetBytes(data, CFRangeMake(0, bufferLength), buffer);
        threadPortName = CFStringCreateWithBytes (NULL, buffer, bufferLength, kCFStringEncodingASCII, FALSE);
 
        // You must obtain a remote message port by name.
        messagePort = CFMessagePortCreateRemote(NULL, (CFStringRef)threadPortName);
 
        if (messagePort)
        {
            // Retain and save the thread’s comm port for future reference.
            AddPortToListOfActiveThreads(messagePort);
 
            // Since the port is retained by the previous function, release
            // it here.
            CFRelease(messagePort);
        }
 
        // Clean up.
        CFRelease(threadPortName);
        CFAllocatorDeallocate(NULL, buffer);
    }
    else
    {
        // Process other messages.
    }
 
    return NULL;
}

在配置主线程之后,唯一剩下的就是新创建的工作线程创建自己的端口并签入。下面代码显示了工作线程的入口点函数。 该函数提取主线程的端口名称,并使用它来创建一个远程连接回主线程。 该函数然后为其自身创建本地端口,将端口安装在线程的运行循环上,并向包含本地端口名称的主线程发送签入消息。

//Setting up the thread structures

OSStatus ServerThreadEntryPoint(void* param)
{
    // Create the remote port to the main thread.
    CFMessagePortRef mainThreadPort;
    CFStringRef portName = (CFStringRef)param;
 
    mainThreadPort = CFMessagePortCreateRemote(NULL, portName);
 
    // Free the string that was passed in param.
    CFRelease(portName);
 
    // Create a port for the worker thread.
    CFStringRef myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.MyApp.Thread-%d"), MPCurrentTaskID());
 
    // Store the port in this thread’s context info for later reference.
    CFMessagePortContext context = {0, mainThreadPort, NULL, NULL, NULL};
    Boolean shouldFreeInfo;
    Boolean shouldAbort = TRUE;
 
    CFMessagePortRef myPort = CFMessagePortCreateLocal(NULL,
                myPortName,
                &ProcessClientRequest,
                &context,
                &shouldFreeInfo);
 
    if (shouldFreeInfo)
    {
        // Couldn't create a local port, so kill the thread.
        MPExit(0);
    }
 
    CFRunLoopSourceRef rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
    if (!rlSource)
    {
        // Couldn't create a local port, so kill the thread.
        MPExit(0);
    }
 
    // Add the source to the current run loop.
    CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
 
    // Once installed, these can be freed.
    CFRelease(myPort);
    CFRelease(rlSource);
 
    // Package up the port name and send the check-in message.
    CFDataRef returnData = nil;
    CFDataRef outData;
    CFIndex stringLength = CFStringGetLength(myPortName);
    UInt8* buffer = CFAllocatorAllocate(NULL, stringLength, 0);
 
    CFStringGetBytes(myPortName,
                CFRangeMake(0,stringLength),
                kCFStringEncodingASCII,
                0,
                FALSE,
                buffer,
                stringLength,
                NULL);
 
    outData = CFDataCreate(NULL, buffer, stringLength);
 
    CFMessagePortSendRequest(mainThreadPort, kCheckinMessage, outData, 0.1, 0.0, NULL, NULL);
 
    // Clean up thread data structures.
    CFRelease(outData);
    CFAllocatorDeallocate(NULL, buffer);
 
    // Enter the run loop.
    CFRunLoopRun();
}

一旦进入其运行循环,发送到线程端口的所有未来事件都将由ProcessClientRequest函数处理。 该功能的实现取决于线程工作的类型。

后记

未完,待续~~

上一篇 下一篇

猜你喜欢

热点阅读