如何在 NSOperationQueue 上使用 NSRunLoop?

How do I use an NSRunLoop on an NSOperationQueue?

我有一个通过蓝牙与 ExternalAccessory 通信的应用程序,响应有一些延迟,所以我希望 IO 在后台线程上发生。

我为单线程操作设置了一个 NSOperationQueue 来排队我的请求:

self.sessionQueue = [NSOperationQueue new];
self.sessionQueue.maxConcurrentOperationCount = 1;

如果我安排读取和写入来自该队列的 EAAccessory 流,我的应用程序会崩溃,因为如果队列所在的线程上没有 NSRunLoop,则无法传送来自套接字的数据使用。初始化队列后,我立即创建一个 运行 循环,其中包含一个空 NSMachPort 以保持它 运行ning 并启动它:

[self.sessionQueue addOperationWithBlock:^{
    NSRunLoop* queueLoop = [NSRunLoop currentRunLoop];
    [queueLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
    [queueLoop run]; // <- this blocks
}];

这会阻塞队列,因为 运行 循环永远不会退出,但我不确定如何正确管理 运行 循环,以便我可以成功地从辅助流中读取。

您不应该尝试在 NSOperation 中 运行 运行 循环。 Grand Central Dispatch 拥有正在执行 运行ning 操作的线程。您应该启动自己的线程并将其 运行 循环用于您的会话流。

However, you need to be aware that NSRunLoop is not generally thread safe, but CFRunLoop is. 这意味着当您想 运行 会话处理线程上的一个块时,您需要下降到 CFRunLoop 级别。

此外,获取对后台线程的 运行 循环的引用的唯一方法是 运行 该后台线程上的某些内容。所以第一步是创建自己的 NSThread 子类,导出自己的 运行 循环:

typedef void (^MyThreadStartCallback)(CFRunLoopRef runLoop);

@interface MyThread: NSThread

/// After I'm started, I dispatch to the main queue to call `callback`,
// passing my runloop. Then I destroy my reference to `callback`.
- (instancetype)initWithCallback:(MyThreadStartCallback)callback;

@end

@implementation MyThread {
    MyThreadStartCallback _callback;
}

- (instancetype)initWithCallback:(MyThreadStartCallback)callback {
    if (self = [super init]) {
        _callback = callback;
    }
    return self;
}

- (void)main {
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    dispatch_async(dispatch_get_main_queue(), ^{
        _callback(runLoop);
    });
    _callback = nil;
    CFRunLoopRun();
}

@end

现在您可以创建 MyThread 的实例,传入回调。当您启动 MyThread 时,它将使回调 运行 回到主线程,并将其自己的(MyThread 的)运行 循环传递给回调。所以你可以使用 MyThread 作为你的会话处理线程,像这样:

@implementation Thing {
    CFRunLoopRef _sessionRunLoop;
}

- (void)scheduleStreamsOfSession:(EASession *)session {
    MyThread *thread = [[MyThread alloc] initWithCallback:^(CFRunLoopRef runLoop) {
        // Here I'm on the main thread, but the session-handling thread has
        // started running and its run loop is `runLoop`.
        [self scheduleStreamsOfSession:session inRunLoop:runLoop];
    }];
    [thread start];
}

- (void)scheduleStreamsOfSession:(EASession *)session inRunLoop:(CFRunLoopRef)runLoop {

    // Here I'm on the main thread. I'll save away the session-handling run loop
    // so I can run more blocks on it later, perhaps to queue data for writing
    // to the output stream.
    _sessionRunLoop = runLoop;

    NSInputStream *inputStream = session.inputStream;
    NSOutputStream *outputStream = session.outputStream;

    // Here I'm on the main thread, where it's not safe to use the
    // session-handling thread's NSRunLoop, so I'll send a block to
    // the session-handling thread.
    CFRunLoopPerformBlock(runLoop, kCFRunLoopCommonModes, ^{

        // Here I'm on the session-handling thread, where it's safe to
        // use NSRunLoop to schedule the streams.
        NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
        [inputStream scheduleInRunLoop:currentRunLoop forMode:NSRunLoopCommonModes];
        [outputStream scheduleInRunLoop:currentRunLoop forMode:NSRunLoopCommonModes];

    });

    // CFRunLoopPerformBlock does **not** wake up the run loop. Since I want
    // to make sure the block runs as soon as possible, I have to wake up the
    // run loop manually:
    CFRunLoopWakeUp(_sessionRunLoop);
}

@end

如果需要,任何线程都可以为其创建一个 NSRunLoop,任何 Cocoa 或 AppKit 应用程序的 main 线程默认都有一个 运行ning,任何辅助线程必须以编程方式 运行 它们。如果你正在生成一个 NSThread 线程体将负责启动 NSRunLoop 但一个 NSOperationQueue 会创建它自己的一个或多个线程并向它们分派操作。

当使用 API 时,它期望 NSRunLoop 将事件传递到后台线程或从后台线程传递事件,无论是您自己创建的还是 libdispatch 创建的,您都是负责确保 NSRunLoop 是 运行。通常你会想要 运行 循环,直到在你的每个 NSBlockOperation 任务中满足某些条件,我在 NSRunLoop 上写了一个类别来简化这个:

#import <Foundation/Foundation.h>

@interface NSRunLoop (Conditional)
-(BOOL)runWhileCondition:(BOOL *)condition inMode:(NSString *)mode inIntervals:(NSTimeInterval) quantum;
@end

#pragma mark -

@implementation NSRunLoop (Conditional)

-(BOOL)runWhileCondition:(BOOL *)condition inMode:(NSString *)mode inIntervals:(NSTimeInterval) quantum {
    BOOL didRun = NO;
    BOOL shouldRun = YES;
    NSPort *dummyPort = [NSMachPort port];
    [self addPort:dummyPort forMode:NSDefaultRunLoopMode];
    while (shouldRun) {
        @autoreleasepool {
            didRun = [self runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:quantum]];
            shouldRun = (didRun ? *condition : NO);
        }
    }
    [self removePort:dummyPort forMode:NSDefaultRunLoopMode];
    return didRun;
}

@end

有了这个条件,您可以安排一个 NSBlockOperation,它将启动 运行 循环和 运行,直到指定的条件为 NO:

__block BOOL streamOperationInProgress = YES;
[self.sessionQueue addOperationWithBlock:^{
    NSRunLoop *queueLoop = [NSRunLoop currentRunLoop];
    NSStream *someStream = // from somewhere...
    [someStream setDelegate:self];
    [someStream scheduleInRunLoop:queueLoop forMode:NSDefaultRunLoopMode]:

    // the delegate implementation of stream:handleEvent:
    // sets streamOperationInProgress = NO;

    [queueLoop
        runWhileCondition:&streamOperationInProgress 
        inMode:NSDefaultRunLoopMode 
        inIntervals:0.001];
}];

上面示例中的问题是将 BOOL 放在某个地方,委托可以在操作完成时将其设置为 NO

这是 NSRunLoop+Condition 类别的要点。