NSURLSession与NSBlockOperation和队列

55

我有一个应用程序,目前在大部分的网络请求中使用NSURLConnection。由于苹果告诉我这是正确的方法,我想转换到NSURLSession

我的应用程序只使用+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error类方法作为NSURLConnection同步版本。我在NSOperationQueue上运行的NSBlockOperation内执行此操作,因此不会无谓地阻塞主队列。以这种方式操作的重要优点是,我可以让任务相互依赖。例如,我可以让请求数据的任务依赖于登录任务完成。

我没有看到NSURLSession支持同步操作。我只能找到一些文章批评我甚至想同步使用它,而且我是一个可怕的人,会阻塞线程。好吧。但我没有看到如何使NSURLSessionTask相互依赖。有没有办法做到这一点?

或者是否有关于以不同方式完成此操作的描述?


2
有一个地方,同步的NSURLSession非常有用,也是最简单的方法。当编写一个与Web交互的命令行实用程序时,除非您打算同时启动多个请求,否则没有理由使用异步方式。 为了使其同步,我在其周围添加了一个信号量锁。如果没有这个锁,应用程序将在请求完成之前退出,因为没有其他东西(即GUI运行循环)来保持应用程序活动状态。绝大多数iOS / OS / X程序员都不这样做,因此这个主题并不经常出现。 - Eric apRhys
4
谢谢你(以及感谢答案的@Rob)。无论我看到哪里,都只能看到一群保姆在抱怨如何永远不应该做同步请求,而不是回答问题。有时候你需要同步-就像我这个情况,我正在处理一个第三方库,它会回调我的代码,我的代码需要进行URL请求,在请求完成之前不能返回给库。 - Jeff Loughlin
Rob的回答很好。我也曾对我偶然发现的“只需使用async”这种答案的数量感到沮丧。如果有人想要一个易于消费、可直接替代NSURLConnection sendSynchronousRequest:的解决方案,我已经将被接受的答案整理成了iOS类别 - aroth
3个回答

108
同步网络请求最受严厉批评的是那些在主队列上执行的请求(因为我们知道不应该阻塞主队列)。但是您正在自己的后台队列上执行它,这解决了同步请求中最严重的问题。但是您将失去一些异步技术提供的精彩功能(例如,如有需要,取消请求)。
我将在下面回答您的问题(如何使NSURLSessionDataTask表现为同步),但我真的鼓励您采用异步模式而不是反对它们。我建议您重构代码以使用异步模式。具体而言,如果一个任务依赖于另一个任务,则只需将依赖任务的启动放在先前任务的完成处理程序中即可。
如果您在转换过程中遇到问题,请发布另一个 Stack Overflow 问题,向我们展示您尝试的内容,我们可以帮助您解决问题。
如果您想将异步操作变成同步操作,一种常见的模式是使用调度信号量,以便启动异步进程的线程可以在异步操作的完成块中等待信号后继续执行。永远不要从主队列执行此操作,但如果您是从某个后台队列执行此操作,则可以使用此有用的模式。
您可以使用以下代码创建信号量:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

然后,您可以通过异步处理的完成块使用以下代码向信号量发出信号:

dispatch_semaphore_signal(semaphore);

接着,您可以让完成块以外的代码(仍在后台队列而不是主队列上)等待该信号:

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

因此,使用NSURLSessionDataTask,将所有内容放在一起,可能会看起来像:

[queue addOperationWithBlock:^{

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    NSURLSession *session = [NSURLSession sharedSession]; // or create your own session with your own NSURLSessionConfiguration
    NSURLSessionTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (data) {
            // do whatever you want with the data here
        } else {
            NSLog(@"error = %@", error);
        }

        dispatch_semaphore_signal(semaphore);
    }];
    [task resume];

    // but have the thread wait until the task is done

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    // now carry on with other stuff contingent upon what you did above
]);

使用NSURLConnection(现已弃用),您必须跨越一些障碍才能从后台队列启动请求,但NSURLSession可以优雅地处理它。


话虽如此,使用像这样的块操作意味着操作不会响应取消事件(至少在运行时不会)。因此,我通常避开这个信号量技术,而是将数据任务包装在异步的NSOperation子类中。然后您可以享受操作的好处,但也可以使它们可取消。这更费力,但是更好的模式。

例如:

//
//  DataTaskOperation.h
//
//  Created by Robert Ryan on 12/12/15.
//  Copyright © 2015 Robert Ryan. All rights reserved.
//

@import Foundation;
#import "AsynchronousOperation.h"

NS_ASSUME_NONNULL_BEGIN

@interface DataTaskOperation : AsynchronousOperation

/// Creates a operation that retrieves the contents of a URL based on the specified URL request object, and calls a handler upon completion.
///
/// @param  request                    A NSURLRequest object that provides the URL, cache policy, request type, body data or body stream, and so on.
/// @param  dataTaskCompletionHandler  The completion handler to call when the load request is complete. This handler is executed on the delegate queue. This completion handler takes the following parameters:
///
/// @returns                           The new session data operation.

- (instancetype)initWithRequest:(NSURLRequest *)request dataTaskCompletionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))dataTaskCompletionHandler;

/// Creates a operation that retrieves the contents of a URL based on the specified URL request object, and calls a handler upon completion.
///
/// @param  url                        A NSURL object that provides the URL, cache policy, request type, body data or body stream, and so on.
/// @param  dataTaskCompletionHandler  The completion handler to call when the load request is complete. This handler is executed on the delegate queue. This completion handler takes the following parameters:
///
/// @returns                           The new session data operation.

- (instancetype)initWithURL:(NSURL *)url dataTaskCompletionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))dataTaskCompletionHandler;

@end

NS_ASSUME_NONNULL_END

并且
//
//  DataTaskOperation.m
//
//  Created by Robert Ryan on 12/12/15.
//  Copyright © 2015 Robert Ryan. All rights reserved.
//

#import "DataTaskOperation.h"

@interface DataTaskOperation ()

@property (nonatomic, strong) NSURLRequest *request;
@property (nonatomic, weak) NSURLSessionTask *task;
@property (nonatomic, copy) void (^dataTaskCompletionHandler)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error);

@end

@implementation DataTaskOperation

- (instancetype)initWithRequest:(NSURLRequest *)request dataTaskCompletionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))dataTaskCompletionHandler {
    self = [super init];
    if (self) {
        self.request = request;
        self.dataTaskCompletionHandler = dataTaskCompletionHandler;
    }
    return self;
}

- (instancetype)initWithURL:(NSURL *)url dataTaskCompletionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))dataTaskCompletionHandler {
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    return [self initWithRequest:request dataTaskCompletionHandler:dataTaskCompletionHandler];
}

- (void)main {
    NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:self.request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        self.dataTaskCompletionHandler(data, response, error);
        [self completeOperation];
    }];

    [task resume];
    self.task = task;
}

- (void)completeOperation {
    self.dataTaskCompletionHandler = nil;
    [super completeOperation];
}

- (void)cancel {
    [self.task cancel];
    [super cancel];
}

@end

含义:

//
//  AsynchronousOperation.h
//

@import Foundation;

@interface AsynchronousOperation : NSOperation

/// Complete the asynchronous operation.
///
/// This also triggers the necessary KVO to support asynchronous operations.

- (void)completeOperation;

@end

并且

//
//  AsynchronousOperation.m
//

#import "AsynchronousOperation.h"

@interface AsynchronousOperation ()

@property (nonatomic, getter = isFinished, readwrite)  BOOL finished;
@property (nonatomic, getter = isExecuting, readwrite) BOOL executing;

@end

@implementation AsynchronousOperation

@synthesize finished  = _finished;
@synthesize executing = _executing;

- (instancetype)init {
    self = [super init];
    if (self) {
        _finished  = NO;
        _executing = NO;
    }
    return self;
}

- (void)start {
    if ([self isCancelled]) {
        self.finished = YES;
        return;
    }

    self.executing = YES;

    [self main];
}

- (void)completeOperation {
    self.executing = NO;
    self.finished  = YES;
}

#pragma mark - NSOperation methods

- (BOOL)isAsynchronous {
    return YES;
}

- (BOOL)isExecuting {
    @synchronized(self) {
        return _executing;
    }
}

- (BOOL)isFinished {
    @synchronized(self) {
        return _finished;
    }
}

- (void)setExecuting:(BOOL)executing {
    @synchronized(self) {
        if (_executing != executing) {
            [self willChangeValueForKey:@"isExecuting"];
            _executing = executing;
            [self didChangeValueForKey:@"isExecuting"];
        }
    }
}

- (void)setFinished:(BOOL)finished {
    @synchronized(self) {
        if (_finished != finished) {
            [self willChangeValueForKey:@"isFinished"];
            _finished = finished;
            [self didChangeValueForKey:@"isFinished"];
        }
    }
}

@end

你展示的方法看起来很有潜力,但是我决定听从你的其他建议,并花了些时间来进行异步处理。我的最大挑战是尝试复制依赖关系。最终,我使用了NSCondition来模拟该功能,但我对自己的实现并不自信。如果NSURLSessionDataTask支持NSOperation,我就可以调用addDependency了,那会很好! - Erik Allen
2
@ErikAllen 没有任何阻止您将 NSURLSessionTask 包装在并发的 NSOperation 子类中,然后享受依赖关系(如果进行许多并发请求,则可以控制并发度)。如果您只想要一个简单的“当登录任务完成后,启动另一个任务”,则可以使用带有 completionHandlerdataTaskWithURL 版本来执行登录任务,并在 completionHandler 中启动下一个任务。 - Rob
1
这是一个非常好的答案,因为前几段给出了很好的建议。不需要阅读其余部分。如果可能的话,我也赞成重构。 - Mazyod
1
如果我有4个依赖任务,A -> B -> C -> D,将完成处理程序嵌套在完成处理程序内部的3个级别内是否是良好的实践? - Arthur Thompson
1
@ArthurThompson - 如果我有那种嵌套,我可能会建议分别具有指示其角色的名称的函数。例如,A可以是“登录”,B可以是“检索目录”,C可以是“检索详细信息”,D可以是“检索图像”。然后,每个函数可能只调用其完成处理程序中的下一个函数,解决可怕的嵌套并使其更加易于理解。另一种方法是异步操作方法,然后您可以创建四个操作,并在它们之间声明依赖关系或将它们添加到串行队列中。 - Rob
显示剩余2条评论

2

@Rob,我建议你将回复作为解决方案发布,因为NSURLSession.dataTaskWithURL(_:completionHandler:)的以下文档注释:

该方法旨在成为NSURLConnection的sendAsynchronousRequest:queue:completionHandler:方法的替代品,具有支持自定义身份验证和取消的功能。


0
如果信号量的方法不起作用,可以尝试基于轮询的方法。
var reply = Data()
/// We need to make a session object.
/// This is key to make this work. This won't work with shared session.
let conf = URLSessionConfiguration.ephemeral
let sess = URLSession(configuration: conf)
let task = sess.dataTask(with: u) { data, _, _ in
    reply = data ?? Data()
}
task.resume()
while task.state != .completed {
    Thread.sleep(forTimeInterval: 0.1)
}
FileHandle.standardOutput.write(reply)

基于轮询的方法非常可靠,但有效地将最大吞吐量限制为轮询间隔。在这个例子中,它被限制为每秒10次。


基于信号量的方法到目前为止一直运行良好,但自 Xcode 11 时代以来,出现了故障。(也许只有我有这个问题?)

如果我等待信号量,数据任务不会完成。如果我在不同的线程上等待信号量,则任务将失败并出现错误。

nw_connection_copy_protocol_metadata [C2] Client called nw_connection_copy_protocol_metadata on unconnected nw_connection error.

看起来实现方面有所改变,因为苹果正在转向 Network.framework


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接