使用NSBlockOperation和队列的NSURLSession

时间:2014-01-18 00:22:08

标签: ios objective-c nsurlsession nsblockoperation

我有一个应用程序,目前使用NSURLConnection用于绝大多数网络。我想转到NSURLSession,因为Apple告诉我这是要走的路。

我的应用只是通过NSURLConnection类方法使用+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error的同步版本。我是在NSBlockOperation NSOperationQueue上运行的,所以我不会不必要地阻止主队列。以这种方式做事的最大好处是我可以使操作彼此依赖。例如,我可以让请求数据的任务依赖于登录任务完成。

我在NSURLSession内没有看到对同步操作的任何支持。所有我能找到的文章都是嘲笑我甚至想要同步使用它而且我是一个阻止线程的可怕人物。精细。但我认为没有办法让NSURLSessionTask彼此依赖。有没有办法做到这一点?

或者有描述我将如何以不同的方式做这样的事情吗?

3 个答案:

答案 0 :(得分:107)

同步网络请求的最严厉批评是为那些从主队列执行的人保留的(因为我们知道永远不应该阻塞主队列)。但是你在自己的后台队列中进行,这解决了同步请求中最令人震惊的问题。但是,您正在失去异步技术提供的一些很棒的功能(例如,如果需要,可以取消请求)。

我将在下面回答您的问题(如何使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

答案 1 :(得分:2)

@Rob我建议您根据NSURLSession.dataTaskWithURL(_:completionHandler:)的以下文档说明将您的回复作为解决方案发布:

  

此方法旨在替代   sendAsynchronousRequest:queue:completionHandler:方法   NSURLConnection,增加了支持自定义的功能   验证和取消。

答案 2 :(得分: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时,实施方式似乎有所改变。