版本记录
| 版本号 | 时间 |
|---|---|
| V1.0 | 2017.08.24 |
前言
NSRunloop是OC 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中,您可以使用以下任一类方法一次创建和调度计时器:
- scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
- scheduledTimerWithTimeInterval:invocation:repeats:
这些方法创建定时器,并以默认模式(NSDefaultRunLoopMode)将其添加到当前线程的运行循环中。 你也可以手动调度计时器,如果您想通过创建NSTimer对象然后使用NSRunLoop的addTimer: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的输入源
Cocoa和Core 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函数处理。 该功能的实现取决于线程工作的类型。
后记
未完,待续~~
