10

我试图更好地理解操作和线程,并查看了 AFNetworking 的AFURLConnectionOperation子类,例如真实世界的源代码。

我目前的理解是,当将 的实例NSOperation添加到操作队列时,队列等管理负责执行操作的线程。在 Apple 的文档中NSOperation指出,即使子类返回YES操作-isConcurrent也将始终在单独的线程上启动(从 10.6 开始)。

基于 Apple 在Thread Programming GuideConcurrency Programming Guide中的强大语言,似乎管理线程最好留给NSOperationQueue.

然而,AFNetworking 的AFURLConnectionOperation子类产生了一个新NSThread的 ,并且操作-main方法的执行被推到这个网络请求线程上。为什么?为什么这个网络请求线程是必要的?这是一种防御性编程技术,因为该库旨在供广大受众使用吗?图书馆的消费者调试的麻烦是否更少?在专用线程上进行所有网络活动是否有(微妙的)性能优势?

(1 月 26 日添加)
在 Dave Dribin 的博客文章中,他说明了如何使用 NSURLConnection 的特定示例将操作移回主线程。

我的好奇心来自 Apple's Thread Programming Guide中的以下部分:

保持线程合理繁忙。
如果您决定手动创建和管理线程,请记住线程会消耗宝贵的系统资源。你应该尽你最大的努力确保你分配给线程的任何任务都是合理的长寿命和高效的。同时,您不应该害怕终止大部分时间处于空闲状态的线程。线程使用大量内存,其中一些是有线的,因此释放空闲线程不仅有助于减少应用程序的内存占用,还可以释放更多物理内存供其他系统进程使用。

在我看来,AFNetworking 的网络请求线程并没有“保持相当忙碌”;它正在运行一个无限循环来处理网络 I/O。但是,看,这就是这个问题的重点——我不知道,我只是猜测。

任何关于操作、线程(运行循环?)和/或 GCD 的特定见解或解构AFURLConnectionOperation都将非常有助于填补我的理解空白。

4

1 回答 1

6

这是一个有趣的问题,答案是关于如何交互和协同工作的NSOperation语义NSURLConnection

AnNSURLConnection本身就是一个异步任务。这一切都发生在后台,并定期调用其委托与结果。当你启动NSURLConnection它时,它使用调度它的 runloop 来调度委托回调,因此 runloop 必须始终在你正在执行的线程上运行NSURLConnection

因此-start,我们的方法AFURLConnectionOperation总是必须在操作完成之前返回,以便它可以接收回调。这要求这AFURLConnectionOperation是一个异步操作。

来自:https ://developer.apple.com/library/mac/documentation/Cocoa/Reference/NSOperation_class/index.html

对于相对于当前线程异步运行的操作,property 的值为 YES,对于在当前线程上同步运行的操作,property 的值为 NO。此属性的默认值为 NO。

AFURLConnectionOperation会覆盖此方法并按YES我们的预期返回。然后我们从类描述中看到:

当您调用异步操作的 start 方法时,该方法可能会在相应任务完成之前返回。异步操作对象负责在单独的线程上调度其任务。该操作可以通过直接启动新线程、调用异步方法或将块提交到调度队列执行来实现。当控制权返回给调用者时操作是否正在进行实际上并不重要,只是它可能正在进行。

AFNetworking 使用一个类方法创建一个单一的网络线程,它调度所有NSURLConnection对象(及其产生的委托回调)。这是来自的代码AFURLConnectionOperation

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });

    return _networkRequestThread;
}

这是AFURLConnectionOperation显示他们NSURLConnection在所有运行循环模式下调度 AFNetwokring 线程的运行循环的代码

- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;

        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

- (void)operationDidStart {
    [self.lock lock];
    if (![self isCancelled]) {
        self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        for (NSString *runLoopMode in self.runLoopModes) {
            [self.connection scheduleInRunLoop:runLoop forMode:runLoopMode];
            [self.outputStream scheduleInRunLoop:runLoop forMode:runLoopMode];
        }

        //...
    }
    [self.lock unlock];
}

这里[NSRunloop currentRunloop]检索 AFNetworking 线程上的运行循环,而不是从mainRunloop-operationDidStart线程调用方法。作为奖励,我们也可以outputStream在后台线程的 runloop 上运行。

现在AFURLConnectionOperation等待回调并随着网络请求的进行NSURLConnection更新自己的NSOperation状态变量(cancelled, finished, )。executingAFNetworking 线程反复旋转它的运行循环,以便NSURLConnections从潜在的许多AFURLConnectionOperations调度它们的回调中调用它们并且AFURLConnectionOperation对象可以对它们做出反应。

如果您总是计划使用队列来执行您的操作,将它们定义为同步会更简单。但是,如果您手动执行操作,您可能希望将操作对象定义为异步的。定义异步操作需要做更多的工作,因为您必须监控任务的持续状态并使用 KVO 通知报告该状态的变化。但是,在您希望确保手动执行的操作不会阻塞调用线程的情况下,定义异步操作很有用。

另请注意,您也可以通过调用并观察它直到返回来使用NSOperation不带 a的 a 。如果被实现为同步操作并阻塞了等待完成的当前线程,它将永远不会真正完成,因为它将在当前运行循环上安排它的回调,因为我们会阻塞它而不会运行。因此,为了支持这种有效的使用场景,我们必须使异步。NSOperationQueue-start-isFinishedYESAFURLConnectionOperationNSURLConnectionNSURLConnectionNSOperationAFURLConnectionOperation

问题的答案

  • 是的,AFNetworking 创建了一个线程,用于调度所有连接。线程创建是昂贵的。(这就是创建 GCD 的部分原因。GCD 为您保留一个运行的线程池,并根据需要在不同的线程上分派块,而无需自己创建、销毁和管理线程)

  • 该处理不在后台 AFNetworking 线程上完成。AFNetworking 使用 的completionBlock属性进行NSOperation处理,当finished设置为时执行YES

无法保证完成块的确切执行上下文,但通常是辅助线程。因此,您不应使用此块来执行任何需要非常特定的执行上下文的工作。相反,您应该将该工作分流到应用程序的主线程或能够执行此操作的特定线程。例如,如果您有一个自定义线程来协调操作的完成,您可以使用完成块来 ping 该线程。

HTTP 连接的后处理在AFHTTPRequestOperation. 此类创建一个调度队列,专门用于在后台转换响应对象,并将工作分流到该队列上。看这里

- (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
                              failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
    self.completionBlock = ^{
        //...
        dispatch_async(http_request_operation_processing_queue(), ^{
            //...

我想这引出了一个问题,他们是否可以写信AFURLConnectionOperation不做一个线程。我认为答案是肯定的,因为有这个 API

- (void)setDelegateQueue:(NSOperationQueue*) queue NS_AVAILABLE(10_7, 5_0);

这意味着将您的委托回调安排在特定的操作队列上,而不是使用运行循环。但是当我们看到 AFNetworking 的遗留部分时,该 API 仅在 iOS 5 和 OS X 10.7 中可用。查看 Github 上的责备视图,AFURLRequestOperation我们可以看到,mattt 实际上+networkRequestThread在 2011 年 iPhone 4s 和 iOS 5 发布的那一天巧合地编写了该方法!因此,我们可以推断线程存在,因为在编写线程时,我们可以看到创建线程并在其上调度连接是NSURLConnection在异步子类中运行时从后台接收回调的唯一方法NSOperation

  • 线程是使用该dispatch_once函数创建的。(请参阅我按照您的建议添加的额外代码)此功能确保包含在它运行的块中的代码将在应用程序的生命周期中仅运行一次。AFNetworking 线程在需要时创建,然后在应用程序的生命周期内持续存在

  • 当我写的时候NSURLConnectionOperation,我的意思是AFURLConnectionOperation。我更正了,谢谢你提到它:)

于 2015-01-07T17:26:29.260 回答