【问题标题】:NSURLSession with NSBlockOperation and queues带有 NSBlockOperation 和队列的 NSURLSession
【发布时间】:2014-02-07 12:31:08
【问题描述】:

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

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

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

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

【问题讨论】:

  • 同步 NSURLSession 有一个地方非常有用,也是最简单的方法。在编写与 Web 交互的命令行实用程序时,没有理由使用异步,除非您打算一次衍生多个请求。为了使其同步,我在它周围添加了一个信号量锁。没有它,应用程序将在请求完成之前退出,因为没有其他东西(即 GUI 运行循环)来保持应用程序处于活动状态。绝大多数 iOS/OS/X 程序员不这样做,所以这个话题很少出现。
  • 感谢这个(并感谢@Rob 的回答)。在我所见的任何地方,我所看到的都是一群保姆抱怨你永远不应该做同步请求,而不是回答问题。有时您需要同步 - 在我的情况下,我正在处理一个对我的代码进行回调的第三方库,它需要执行 url 请求并且在请求完成之前不返回库。跨度>
  • Rob 的回答很棒。我也对偶然发现“只使用异步”的答案数量感到沮丧。如果有人想要一个易于使用的、直接替换 NSURLConnection sendSynchronousRequest: 的插件,我已将接受的答案汇总为 iOS Category

标签: ios objective-c nsurlsession nsblockoperation


【解决方案1】:

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

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,实施中似乎发生了一些变化。

【讨论】:

    【解决方案2】:

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

    我将在下面回答您的问题(如何使 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。 :-)
    • @ErikAllen 绝对没有什么可以阻止您将NSURLSessionTask 包装在并发的NSOperation 子类中,然后享受依赖关系(如果执行许多并发请求,则控制并发程度)。如果您只想要一个简单的“登录任务完成后,开始另一个任务”,您可以使用dataTaskWithURLcompletionHandler 的再现来执行登录任务,并在`completionHandler 中启动下一个任务。
    • 由于前几段中的建议,这是一个非常好的答案。其余部分无需阅读。如果可能的话,我也支持重构。
    • 如果我有 4 个依赖任务,A -> B -> C -> D,在完成处理程序内的完成处理程序内使用完成处理程序深入 3 个级别是否是一个好习惯...?
    • @ArthurThompson - 如果我有这种嵌套,我可能会建议使用名称表明其角色的单独函数。例如。 A 可能是“login”,B 可能是“retrieveTableOfContents”,C 可能是“retrieveDetails”,D 可能是“retrieveImages”。然后每个函数可能只是在其完成处理程序中调用下一个函数,解决可怕的嵌套并使其更易于理解。另一种方法是异步操作方法,然后您可以创建四个操作并声明它们之间的依赖关系或将它们添加到串行队列中。
    【解决方案3】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-06-04
      • 1970-01-01
      • 1970-01-01
      • 2016-12-12
      • 2014-10-23
      • 2011-12-22
      相关资源
      最近更新 更多