ios NSURLSession 与 NSBlockOperation 和队列

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/21198404/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 23:10:23  来源:igfitidea点击:

NSURLSession with NSBlockOperation and queues

iosobjective-cnsurlsessionnsblockoperation

提问by Erik Allen

I have an app that currently uses NSURLConnectionfor the vast majority of its networking. I would like to move to NSURLSessionbecause Apple tells me that is the way to go.

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

My app just uses the synchronous version of NSURLConnectionby way of the + (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)errorclass method. I do this within a NSBlockOperationrunning on an NSOperationQueueso I am not needlessly blocking the main queue. The big advantage to doing things this way is that I can make the operations dependent on one another. For example, I can have the task that is requesting data be dependent on the login task finishing.

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

I have not seen any support for synchronous operations within NSURLSession. All I can find are articles deriding me for even thinking of using it synchronously and that I am a horrible person for blocking the threads. Fine. But I see no way to make NSURLSessionTasks dependent on each other. Is there a way to do that?

我在NSURLSession. 我所能找到的只是嘲笑我甚至考虑同步使用它的文章,并且我是一个阻塞线程的可怕人。美好的。但我认为没有办法让NSURLSessionTasks 相互依赖。有没有办法做到这一点?

Or is there a description of how I would do such a thing in a different way?

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

回答by Rob

The harshest criticisms of synchronous network requests are reserved for those who do it from the main queue (as we know that one should never block the main queue). But you're doing it on your own background queue, which addresses the most egregious problem with synchronous requests. But you're losing some wonderful features that asynchronous techniques provide (e.g. cancelation of requests, if needed).

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

I'll answer your question (how to make NSURLSessionDataTaskbehave synchronously) below, but I'd really encourage you to embrace the asynchronous patterns rather than fighting them. I'd suggest refactoring your code to use asynchronous patterns. Specifically, if one task is dependent upon another, simply put the initiation of the dependent task in the completion handler of the prior task.

我将在下面回答你的问题(如何使NSURLSessionDataTask行为同步),但我真的鼓励你接受异步模式而不是对抗它们。我建议重构您的代码以使用异步模式。具体来说,如果一个任务依赖于另一个任务,只需将依赖任务的启动放在前一个任务的完成处理程序中。

If you have problems in that conversion, then post another Stack Overflow question, showing us what you tried, and we can try to help you out.

如果您在转换过程中遇到问题,请发布另一个 Stack Overflow 问题,向我们展示您的尝试,我们可以尝试帮助您。



If you want to make an asynchronous operation synchronous, a common pattern is to use a dispatch semaphore so your thread that initiated the asynchronous process can wait for a signal from the completion block of the asynchronous operation before continuing. Never do this from the main queue, but if you're doing this from some background queue, it can be a useful pattern.

如果要使异步操作同步,常见的模式是使用分派信号量,以便启动异步进程的线程可以在继续之前等待来自异步操作的完成块的信号。永远不要从主队列执行此操作,但是如果您从某个后台队列执行此操作,这可能是一种有用的模式。

You can create a semaphore with:

您可以使用以下命令创建信号量:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

You can then have the completion block of the asynchronous process signal the semaphore with:

然后,您可以让异步进程的完成块向信号量发送信号:

dispatch_semaphore_signal(semaphore);

And you can then have the code outside of the completion block (but still on the background queue, not the main queue) wait for that signal:

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

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

So, with NSURLSessionDataTask, putting that all together, that might look like:

因此,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
]);

With NSURLConnection(now deprecated), you have to jump through some hoops to initiate requests from a background queue, but NSURLSessionhandles it gracefully.

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



Having said that, using block operations like this means that the operations won't respond to cancellation events (while they're running, at least). So I generally eschew this semaphore technique with block operations and just wrap the data tasks in asynchronous NSOperationsubclass. Then you enjoy the benefits of operations, but you can make them cancelable, too. It's more work, but a much better pattern.

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

For example:

例如:

//
//  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

and

//
//  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

Where:

在哪里:

//
//  AsynchronousOperation.h
//

@import Foundation;

@interface AsynchronousOperation : NSOperation

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

- (void)completeOperation;

@end

And

//
//  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

回答by Andrew Ebling

@Rob I would encourage you to post your reply as a solution, in view of the following documentation note from NSURLSession.dataTaskWithURL(_:completionHandler:):

@Rob 鉴于以下文档说明来自以下文档,我鼓励您将回复作为解决方案发布NSURLSession.dataTaskWithURL(_:completionHandler:)

This method is intended as an alternative to the sendAsynchronousRequest:queue:completionHandler: method of NSURLConnection, with the added ability to support custom authentication and cancellation.

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

回答by Eonil

If semaphore based approach doesn't work, try polling based approach.

如果基于信号量的方法不起作用,请尝试基于轮询的方法。

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)

Polling based approach works very reliably, but effectively limits maximum throughput to polling interval. In this example, it's been limited to 10 times/sec.

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



Semaphore based approach has been worked well so far, but since Xcode 11 era, it's getting broken. (maybe only for me?)

到目前为止,基于信号量的方法运行良好,但从 Xcode 11 时代开始,它就被打破了。(也许只适合我?)

A data task does not finish if I wait for semaphores. If I wait for semaphore on different thread, it task fails with an error.

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

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

It seems something has been changed in the implementation as Apple is moving Network.framework.

随着 Apple 的发展,实施中似乎发生了一些变化Network.framework