NSRunloop简单细说(九)—— 几个重要的问题(三)
版本记录
版本号 | 时间 |
---|---|
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简单细说(八)—— 几个重要的问题(二)
一、 Configuring Run Loop Sources - 配置Run Loop的源
以下部分显示了如何在Cococa
和Core Foundation
中设置不同类型的输入源。
1. Defining a Custom Input Source - 定义一个自定义输入源
创建自定义输入源包括定义以下内容:
- 您希望输入源处理的信息。
- 调度程序让感兴趣的客户知道如何联系您的输入源。
- 执行任何客户端发送请求的处理程序。
- 取消例程,使您的输入源无效。
因为您创建一个自定义输入源来处理自定义信息,所以实际的配置被设计为灵活的。 调度程序,处理程序和取消例程是您自定义输入源几乎总是需要的关键例程。 然而,大多数输入源行为的其余部分发生在这些处理程序例程之外。 例如,您可以定义将数据传递到输入源或者将存在的输入源和其他线程进行通信的机制。
下图显示了自定义输入源的示例配置。 在此示例中,应用程序的主线程维护对输入源的引用、输入源的自定义命令缓冲区以及安装了输入源的运行循环。 当主线程有一个任务需要切换到工作线程时,它会将命令发送到命令缓冲区以及工作线程启动任务所需的任何信息。 (因为主线程和工作线程的输入源都可以访问命令缓冲区,所以该访问必须被同步)。一旦命令被发布,主线程就会通知输入源并唤醒工作线程的运行循环。 在接收到唤醒命令后,运行循环将调用输入源的处理程序,处理命令缓冲区中发现的命令。
以下部分说明了上图中自定义输入源的实现,并显示了您需要实现的关键代码。
Defining the Input Source - 定义输入源
定义自定义输入源需要使用Core Foundation例程来配置运行循环源并将其附加到运行循环。 虽然基本的处理程序是基于C的函数,但这并不排除您为这些函数编写包装器,并使用Objective-C或C ++来实现代码的正文。
上图中引入的输入源使用Objective-C对象来管理命令缓冲区并与运行循环进行协调。 下面代码显示了该对象的定义。 RunLoopSource
对象管理命令缓冲区,并使用该缓冲区从其他线程接收消息。 此列表还显示了RunLoopContext
对象的定义,该对象实际上只是一个用于将RunLoopSource对象和运行循环引用传递给应用程序主线程的容器对象。
//自定义输入源对象的定义
@interface RunLoopSource : NSObject
{
CFRunLoopSourceRef runLoopSource;
NSMutableArray* commands;
}
- (id)init;
- (void)addToCurrentRunLoop;
- (void)invalidate;
// Handler method
- (void)sourceFired;
// Client interface for registering commands to process
- (void)addCommand:(NSInteger)command withData:(id)data;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;
@end
// These are the CFRunLoopSourceRef callback functions.
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
void RunLoopSourcePerformRoutine (void *info);
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
// RunLoopContext is a container object used during registration of the input source.
@interface RunLoopContext : NSObject
{
CFRunLoopRef runLoop;
RunLoopSource* source;
}
@property (readonly) CFRunLoopRef runLoop;
@property (readonly) RunLoopSource* source;
- (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop;
@end
尽管Objective-C
代码管理输入源的自定义数据,但是将输入源附加到运行循环中则需要基于C的回调函数。 当您将运行循环源实际附加到运行循环时,将调用其中的第一个函数,如下面的代码所示。 因为这个输入源只有一个客户端(主线程),所以它使用调度程序函数来发送消息,以便在该线程上向应用程序代理注册自身。 当代理想要与输入源进行通信时,它会使用RunLoopContext
对象中的信息。
//Scheduling a run loop source
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
RunLoopSource* obj = (RunLoopSource*)info;
AppDelegate* del = [AppDelegate sharedAppDelegate];
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
[del performSelectorOnMainThread:@selector(registerSource:)
withObject:theContext waitUntilDone:NO];
}
最重要的回调例程之一是用于在您的输入源发出信号时处理自定义数据。 下面代码显示了与RunLoopSource
对象关联的执行回调例程。 该函数简单地将作业的请求转发到sourceFired
方法,该方法然后处理命令缓冲区中存在的任何命令。
void RunLoopSourcePerformRoutine (void *info)
{
RunLoopSource* obj = (RunLoopSource*)info;
[obj sourceFired];
}
如果您使用CFRunLoopSourceInvalidate
函数从运行循环中删除输入源,系统将调用输入源的取消例程。 您可以使用此例程来通知客户您的输入源不再有效,并且应删除对其的任何引用。 列下面代码显示了注册到RunLoopSource
对象的取消回调例程。 此函数将另一个RunLoopContext
对象发送给应用程序委托,但此时请求委托删除对运行循环源的引用。
//Invalidating an input source
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
RunLoopSource* obj = (RunLoopSource*)info;
AppDelegate* del = [AppDelegate sharedAppDelegate];
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
[del performSelectorOnMainThread:@selector(removeSource:)
withObject:theContext waitUntilDone:YES];
}
Installing the Input Source on the Run Loop - 在运行循环中安装输入源
下面代码显示了RunLoopSource
类的init
和addToCurrentRunLoop
方法。 init方法创建必须实际附加到运行循环的CFRunLoopSourceRef opaque
类型。 它将RunLoopSource
对象本身作为上下文信息,以便回调例程具有指向该对象的指针。 在工作线程调用addToCurrentRunLoop
方法之后,才会安装输入源,此时调用RunLoopSourceScheduleRoutine
回调函数。 一旦将输入源添加到运行循环中,线程就可以运行其运行循环来等待它。
// Installing the run loop source
- (id)init
{
CFRunLoopSourceContext context = {0, self, NULL, NULL, NULL, NULL, NULL,
&RunLoopSourceScheduleRoutine,
RunLoopSourceCancelRoutine,
RunLoopSourcePerformRoutine};
runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
commands = [[NSMutableArray alloc] init];
return self;
}
- (void)addToCurrentRunLoop
{
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
}
Coordinating with Clients of the Input Source - 与客户的输入源协调
为了使您的输入源变得有用,您需要操作它并从另一个线程发出信号。 输入源的全部要点是将相关线程置于休眠状态,直到有事情要做。 这个事实需要您的应用程序中的其他线程知道输入源,并有一种与之通信的方式。
通知客户您的输入源的一种方法是在您的输入源首次安装在其运行循环中时发出注册请求。 您可以根据需要向任意多的客户注册您的输入源,也可以将其注册到一些中央代理机构,然后将您的输入源转交给感兴趣的客户。 下面代码显示了当调用RunLoopSource
对象的调度程序函数时由应用程序委托定义并调用的注册方法。 此方法接收RunLoopSource
对象提供的RunLoopContext
对象,并将其添加到其源列表中。 此列表还显示了从输入源从其运行循环中删除时用于注销输入源的例程。
// Registering and removing an input source with the application delegate
- (void)registerSource:(RunLoopContext*)sourceInfo;
{
[sourcesToPing addObject:sourceInfo];
}
- (void)removeSource:(RunLoopContext*)sourceInfo
{
id objToRemove = nil;
for (RunLoopContext* context in sourcesToPing)
{
if ([context isEqual:sourceInfo])
{
objToRemove = context;
break;
}
}
if (objToRemove)
[sourcesToPing removeObject:objToRemove];
}
Signaling the Input Source - 给输入源发送信号
在将数据交给输入源后,客户端必须向源发出信号并唤醒其运行循环。 给源发送信号使运行循环知道源可以被处理。 并且因为当信号发生时,线程可能已经睡着了,你应该总是明确地唤醒运行循环。 否则可能导致处理输入源的延迟。
下面代码展示的是显示RunLoopSource
对象的fireCommandsOnRunLoop
方法,当客户准备好来处理他们添加到缓冲区的命令的源时,客户端调用此方法。
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
CFRunLoopSourceSignal(runLoopSource);
CFRunLoopWakeUp(runloop);
}
这里还需要注意:您不应该尝试通过发送自定义输入源来处理SIGHUP
或其他类型的进程级信号。 用于唤醒运行循环的Core Foundation
功能不是信号安全的,不应该在应用程序的信号处理程序中使用。
后记
未完,待续~~~