55

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

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

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

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

4

3 回答 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
于 2014-01-18T15:19:46.103 回答
2

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

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

于 2014-11-21T10:06:05.323 回答
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.

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

于 2019-10-15T10:45:40.170 回答