【问题标题】:How do I use NSOperationQueue with NSURLSession?如何将 NSOperationQueue 与 NSURLSession 一起使用?
【发布时间】:2023-12-21 04:55:02
【问题描述】:

我正在尝试构建一个批量图像下载器,可以将图像动态添加到队列中以进行下载,并且我可以了解进度以及下载完成时间。

通过我的阅读,队列功能似乎是 NSOperationQueue 和网络功能的 NSURLSession 似乎是我最好的选择,但我对如何同时使用这两者感到困惑。

我知道我将NSOperation 的实例添加到NSOperationQueue 并且它们会排队。似乎我使用NSURLSessionDownloadTask 创建了一个下载任务,如果我需要多个任务,则创建多个,但我不确定如何将两者放在一起。

NSURLSessionDownloadTaskDelegate 似乎拥有下载进度和完成通知所需的所有信息,但我还需要能够停止特定下载、停止所有下载并处理从下载中返回的数据.

【问题讨论】:

标签: ios objective-c nsoperation nsoperationqueue nsurlsession


【解决方案1】:

这是一个使用 NSOperation 和 NSURLSession 的示例项目: https://github.com/MacMark/Operations-Demo

对于后台会话,您可以通过在此回调中使用会话标识符来恢复 NSOperation:

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler {
   // Restore the NSOperation that matches the identifier
   // Let the NSOperation call the completionHandler when it finishes
}

【讨论】:

    【解决方案2】:

    更新:executingfinishing 属性保存有关当前NSOperation 状态的信息。一旦您将finishing 设置为YES 并将executing 设置为NO,您的操作将被视为已完成。正确的处理方式不需要dispatch_group,可以简单的写成异步的NSOperation

      - (BOOL) isAsynchronous {
         return YES;
      }
    
      - (void) main
        {
           // We are starting everything
           self.executing = YES;
           self.finished = NO;
    
           NSURLSession * session = [NSURLSession sharedInstance];
    
           NSURL *url = [NSURL URLWithString:@"http://someurl"];
    
           NSURLSessionDataTask * dataTask = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
    
              /* Do your stuff here */
    
             NSLog("Will show in second");
    
             self.executing = NO;
             self.finished = YES;
           }];
    
           [dataTask resume]
       }
    

    asynchronous 一词具有很大的误导性,并不是指 UI(主)线程和后台线程之间的区别。

    如果isAsynchronous设置为YES表示部分代码是异步执行的main方法。换一种说法:main方法内部进行了异步调用,该方法将在main方法完成后完成

    我有一些关于如何在苹果操作系统上处理并发的幻灯片:https://speakerdeck.com/yageek/concurrency-on-darwin

    旧答案:你可以试试dispatch_group_t。您可以将它们视为 GCD 的保留计数器。

    想象一下NSOperation 子类的main 方法中的以下代码:

    - (void) main
    {
    
       self.executing = YES;
       self.finished = NO;
    
       // Create a group -> value = 0
       dispatch_group_t group = dispatch_group_create();
    
       NSURLSession * session = [NSURLSession sharedInstance];
    
       NSURL *url = [NSURL URLWithString:@"http://someurl"];
    
        // Enter the group manually -> Value = Value + 1
       dispatch_group_enter(group); ¨
    
       NSURLSessionDataTask * dataTask = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
    
    
          /* Do your stuff here */
    
          NSLog("Will show in first");
    
          //Leave the group manually -> Value = Value - 1
          dispatch_group_leave(group);
       }];
    
       [dataTask resume];
    
      // Wait for the group's value to equals 0
      dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
      NSLog("Will show in second");
    
      self.executing = NO;
      self.finished = YES;
    }
    

    【讨论】:

    • 你不需要dispatch_group,你这里没有组,你需要的是一个信号量。
    • 虽然我不知道asynchronous 属性,但它实际上对添加到队列的操作没有任何影响,仅适用于手动执行操作时。来自文档:“当您将操作添加到操作队列时,队列会忽略异步属性的值,并始终从单独的线程调用 start 方法。”。 developer.apple.com/documentation/foundation/…
    • 另外,executingfinished 是只读属性;您应该重新定义这些属性来控制它们的值。
    【解决方案3】:

    如果您正在使用 OperationQueue 并且不希望每个操作同时创建许多网络请求,您可以在每个操作添加到队列后简单地调用 queue.waitUntilAllOperationsAreFinished()。它们现在只会在前一个完成后执行,显着减少同时网络连接的数量。

    【讨论】:

      【解决方案4】:

      从概念上讲,NSURLSession 是一个操作队列。如果您在完成处理程序上恢复 NSURLSession 任务和断点,堆栈跟踪可能会非常有启发性。

      这是来自忠实的 Ray Wenderlich 在 NSURLSession 上的教程的摘录,其中添加了 NSLog 语句到执行完成处理程序的断点:

      NSURLSession *session = [NSURLSession sharedSession];
      [[session dataTaskWithURL:[NSURL URLWithString:londonWeatherUrl]
                completionHandler:^(NSData *data,
                                    NSURLResponse *response,
                                    NSError *error) {
                  // handle response
                  NSLog(@"Handle response"); // <-- breakpoint here       
      
        }] resume];
      

      在上面,我们可以看到Thread 5 Queue: NSOperationQueue Serial Queue中正在执行的完成处理程序。

      所以,我的猜测是每个 NSURLSession 维护它自己的操作队列,并且添加到会话的每个任务 - 在引擎盖下 - 作为 NSOperation 执行。因此,维护一个控制 NSURLSession 对象或 NSURLSession 任务的操作队列是没有意义的。

      NSURLSessionTask 本身已经提供了等效的方法,例如cancelresumesuspend 等。

      确实,与您自己的 NSOperationQueue 相比,控制更少。但话又说回来,NSURLSession 是一个新类,其目的无疑是为了减轻你的负担。

      底线:如果你想要更少的麻烦 - 但更少的控制 - 并相信 Apple 会代表你胜任地执行网络任务,请使用 NSURLSession。否则,使用 NSURLConnection 和你自己的操作队列滚动你自己的。

      【讨论】:

      • 很好的答案,每个将 NSURLSession 包装在操作队列中的人发现这一点后都会爆炸!您也许可以添加会话(或队列!)中的所有任务都可以使用 [session invalidateAndCancel] 取消。此外,如果您将 nil delegateQueue 传递给 sessionWithConfiguration,我发现这很有趣,这些任务按顺序运行,但如果您传递一个新队列,它们会同时运行。我的猜测是顺序(或串行)是大多数人开始尝试排队时的行为。
      • 使用 NSOperationQueue 的一大好处是您可以在操作之间添加依赖关系。 IE 在操作 2 之前运行操作 1。您不能使用 NSURLSession 执行此操作,因为会话不会公开该队列。
      • 我知道这个问题很老,但是当 BlockOperation 在请求之前完成时添加依赖项有什么好处? URLSession 发出一个异步请求,当请求发生时,块的其余部分继续执行。基于此,如果你有 OP1 和 OP2,OP2 依赖于 OP1,即使对 OP1 的请求还没有完成,它也可以执行。
      • @max-macleod 如果同时触发很多任务,而单个任务需要一些时间,那么后面的任务就会超时。所以在这里排队是必不可少的。
      • 不同意,我们不应该依赖非公开的实现细节,因为这可能随时更改,恕不另行通知。 Apple 不会公开操作队列,因此我们不应该假设某些东西是如何工作的。请参阅NSOperation 上的此 WWDC 会话,其中 URLSessionTasks 包含在 Operation 子类中。 developer.apple.com/videos/play/wwdc2015/226
      【解决方案5】:

      你的直觉是正确的。如果发出许多请求,将NSOperationQueuemaxConcurrentOperationCount 设置为4 或5 会非常有用。否则,如果您发出许多请求(例如,50 张大图像),则在处理慢速网络连接(例如某些蜂窝连接)时可能会遇到超时问题。操作队列也有其他优势(例如依赖关系、分配优先级等),但控制并发程度是关键优势,恕我直言。

      如果您使用基于 completionHandler 的请求,则实现基于操作的解决方案非常简单(这是典型的并发 NSOperation 子类实现;请参阅 @ 的 为并发执行配置操作部分987654321@并发编程指南一章了解更多信息)。

      如果您使用的是基于delegate 的实现,那么事情很快就会变得非常棘手。这是因为NSURLSession 的一个可以理解(但非常烦人)的特性,任务级委托是在会话级实现的。 (想一想:需要不同处理的两个不同请求在共享会话对象上调用相同的委托方法。Egad!)

      可以在操作中包装基于委托的NSURLSessionTask(我和我确定的其他人已经这样做了),但它涉及让会话对象维护字典交叉引用任务标识符的笨拙过程对于任务操作对象,让它将这些任务委托方法传递给任务对象,然后让任务对象符合各种NSURLSessionTask委托协议。这是一个相当大的工作量,因为NSURLSession 没有在会话中提供maxConcurrentOperationCount 风格的功能(更不用说其他NSOperationQueue 的优点,比如依赖项、完成块等)。

      值得指出的是,基于操作的实现对于后台会话来说有点难以启动。应用程序终止后,您的上传/下载任务将继续正常运行(这是一件好事,这是后台请求中相当重要的行为),但是当您的应用程序重新启动时,操作队列及其所有操作都消失了.因此,您必须为后台会话使用纯基于委托的 NSURLSession 实现。

      【讨论】:

      • @Rob- 我对此也有同样的困惑。你能解释一下吗。这是我的问题:*.com/questions/24114093/…。您似乎对此有了更好的理解。我想为 NetworkCommunication 开发一个通用库/框架。不想使用 AFNetworking。所以,我可以了解更多。
      • @Rob 如果我们在操作中创建新的 NSURLSession 而不是共享相同的操作,会有多少开销。如果我们这样做,那么我们就不必担心会话级别的委托会在不同的线程上被调用?
      • @Rob 感谢您的快速回复。我做了一些测试,没有发现任何内存问题。可能需要进行更多测试。
      • @Rob 这是一个很好的答案,它让我想知道:如果基于 completionHandler 的请求更容易,是否有任何理由使用基于委托的请求而不是基于 completionHandler 的请求包裹?我能看到的唯一好处是它可以让您在取消之前访问未完全下载的数据,但是由于这些数据将无效,并且 NSOperation 不允许您暂停/恢复下载,这似乎毫无意义。我缺少一些好处吗?
      • @algal 有很多功能没有委托就无法使用。例如,大型下载的进度更新,以便您可以更新进度视图。或处理身份验证挑战。或识别重定向。或者,如果你正在做一些流协议。所以,肯定有你想要委托的时候。但对于那些你不这样做的情况,生活会容易得多。
      【解决方案6】:

      也许你正在寻找这个:

      http://www.dribin.org/dave/blog/archives/2009/05/05/concurrent_operations/

      这不是“内置”有点奇怪,但如果你想将 NSURL 的东西与 NSOperation 连接起来,看起来你必须在主线程中重用运行循环并使操作成为“并发”操作(“并发”到队列中)。

      虽然在您的情况下 - 如果只是简单的下载,没有后续的依赖操作 - 我不确定使用 NSOperation 会获得什么。

      【讨论】:

      • 我同意它不是内置的有点奇怪,但我认为这是为后台会话做出的牺牲,基于操作的实现没有意义(即任务继续在您的应用程序终止后)。但是对于前台任务,将其包装在 NSOperation 中不仅是可能的,而且恕我直言,建议这样做。
      • “我不确定使用 NSOperation 会获得什么。” - 使用操作队列的主要优点是您可以控制并发程度 (maxConcurrentOperationCount),从而消除可能困扰在慢速网络连接上发出许多并发请求的代码的超时问题。
      • 顺便说一句,您引用的博客文章中有一个重大疏忽,即他没有处理取消的操作(这是我们使用操作队列实现而不是 GCD 的主要原因之一)。有关该博客文章代码示例的一些重要更新,请参阅 this answer 的后半部分。
      【解决方案7】:

      使用 NSURLSession,您无需手动将任何操作添加到队列中。你在 NSURLSession 上使用 - (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request 方法来生成一个数据任务,然后你开始(通过调用 resume 方法)。

      您可以提供操作队列,以便您可以控制队列的属性,也可以根据需要将其用于其他操作。

      您希望对数据任务执行的 NSOperation(即开始、暂停、停止、恢复)执行的任何常规操作。

      要排队下载 50 张图片,您只需创建 50 个数据任务,NSURLSession 将正确排队。

      【讨论】:

      • 如何识别特定的NSURLSessionTasks?在您的示例中,50 将在后台运行(关闭主线程)?他们会产生多个线程吗?如果是这样,我在访问委托方法时会遇到问题吗?
      • 您需要自己跟踪任务。您可以保留它们的字典,其中键是您正在加载的 URL 或其他内容。这 50 个任务将在后台线程中运行,并且委托方法将在您传递给 NSURLSession 构造函数的队列上被回调。请记住,操作队列可以在一个或多个线程上运行。
      • @ReidMain "您可以提供操作队列,以便您可以控制队列的属性。"我相信这是不正确的。您可以提供 委托队列,它控制完成回调的位置。但我认为您不能提供、访问或修改自己执行网络操作的内部队列。除非我错过了什么?
      • @algal NSURLSession 一个队列。当您想要发出一堆请求时创建一个会话,如果您在其中一个请求之后出现错误并想要放弃队列,只需调用 invalidateAndCancel 或 getTasksWithCompletionHandler 并全部取消它们。如果你为 delegateQueue 传递 nil,请求是串行完成的,如果你想同时提供一个操作队列。
      • @malhal "如果你为 delegateQueue 传递 nil,请求是串行完成的,如果你想同时提供一个操作队列" 真的吗?你确定吗?我不明白为什么委托回调队列的并发因子会影响NSURLSession自己的内部任务队列的并发因子?至少对于完成回调,我认为独立队列允许为每个队列选择不相关的并发。