版本记录
版本号 | 时间 |
---|---|
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
函数处理。 该功能的实现取决于线程工作的类型。
后记
未完,待续~~
网友评论