【问题标题】:AFNetworking and background transfersAFNetworking 和后台传输
【发布时间】:2014-02-16 11:27:25
【问题描述】:

我对如何利用新的 iOS 7 NSURLSession 后台传输功能和 AFNetworking(版本 2 和 3)有点困惑。

我看到了WWDC 705 - What’s New in Foundation Networking 会话,他们演示了在应用程序终止甚至崩溃后继续进行后台下载。

这是使用新的 API application:handleEventsForBackgroundURLSession:completionHandler: 完成的,并且会话的委托最终将获得回调并可以完成其任务。

所以我想知道如何将它与 AFNetworking(如果可能)一起使用以继续在后台下载。

问题是,AFNetworking 方便地使用基于块的 API 来执行所有请求,但如果应用程序终止或崩溃,这些块也会消失。那么如何才能完成任务呢?

或者我在这里遗漏了一些东西......

让我解释一下我的意思:

例如,我的应用程序是一个照片消息应用程序,假设我有一个代表一条消息的 PhotoMessage 对象,并且该对象具有类似的属性

  • state - 描述照片下载的状态。
  • resourcePath - 最终下载的照片文件的路径。

所以当我从服务器收到一条新消息时,我会创建一个新的PhotoMessage 对象,并开始下载它的照片资源。

PhotoMessage *newPhotoMsg = [[PhotoMessage alloc] initWithInfoFromServer:info];
newPhotoMsg.state = kStateDownloading;

self.photoDownloadTask = [[BGSessionManager sharedManager] downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
    NSURL *filePath = // some file url
    return filePath;
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
    if (!error) {
        // update the PhotoMessage Object
        newPhotoMsg.state = kStateDownloadFinished;
        newPhotoMsg.resourcePath = filePath;
    }
}];

[self.photoDownloadTask resume];   

如您所见,我使用完成块根据收到的响应更新 PhotoMessage 对象。

如何通过后台传输来实现这一点?不会调用此完成块,因此我无法更新 newPhotoMsg

【问题讨论】:

    标签: ios objective-c afnetworking afnetworking-2


    【解决方案1】:

    一些想法:

    1. 您必须确保完成URL 加载系统编程指南Handling iOS Background Activity 部分中列出的必要编码:

      如果您在 iOS 中使用NSURLSession,下载完成后您的应用会自动重新启动。您的应用程序的application:handleEventsForBackgroundURLSession:completionHandler: 应用程序委托方法负责重新创建适当的会话,存储完成处理程序,并在会话调用您的会话委托的URLSessionDidFinishEventsForBackgroundURLSession: 方法时调用该处理程序。

      该指南显示了您可以做什么的一些示例。坦白说,我觉得 WWDC 2013 视频后半部分What’s New in Foundation Networking 讨论的代码示例更加清晰。

    2. 如果应用只是暂停,AFURLSessionManager 的基本实现将与后台会话一起工作(假设您已完成上述操作,您将在网络任务完成时看到您的块被调用)。但正如您所猜测的,“如果应用程序终止或崩溃”,传递给您为上传和下载创建 NSURLSessionTaskAFURLSessionManager 方法的任何特定于任务的块参数都会丢失。

      对于后台上传,这很烦人(因为您在创建任务时指定的任务级信息进度和完成块不会被调用)。但是,如果您使用会话级别的再现(例如 setTaskDidCompleteBlocksetTaskDidSendBodyDataBlock),它将被正确调用(假设您在重新实例化会话管理器时总是设置这些块)。

      事实证明,这个丢块的问题其实对于后台下载来说问题更大,但是那里的解决方法很相似(不要使用基于任务的块参数,而是使用基于会话的块,比如@ 987654331@).

    3. 另一种方法是,您可以坚持使用默认(非后台)NSURLSession,但如果用户在任务进行中离开应用程序,请确保您的应用程序请求一点时间来完成上传。例如,在您创建NSURLSessionTask 之前,您可以创建一个UIBackgroundTaskIdentifier

      UIBackgroundTaskIdentifier __block taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(void) {
          // handle timeout gracefully if you can
      
          [[UIApplication sharedApplication] endBackgroundTask:taskId];
          taskId = UIBackgroundTaskInvalid;
      }];
      

      但要确保网络任务的完成块正确地通知iOS它已经完成:

      if (taskId != UIBackgroundTaskInvalid) {
          [[UIApplication sharedApplication] endBackgroundTask:taskId];
          taskId = UIBackgroundTaskInvalid;
      }
      

      这不如背景NSURLSession 强大(例如,您的可用时间有限),但在某些情况下这可能很有用。


    更新:

    我想我应该添加一个如何使用 AFNetworking 进行后台下载的实际示例。

    1. 首先定义您的后台管理器。

      //
      //  BackgroundSessionManager.h
      //
      //  Created by Robert Ryan on 10/11/14.
      //  Copyright (c) 2014 Robert Ryan. All rights reserved.
      //
      
      #import "AFHTTPSessionManager.h"
      
      @interface BackgroundSessionManager : AFHTTPSessionManager
      
      + (instancetype)sharedManager;
      
      @property (nonatomic, copy) void (^savedCompletionHandler)(void);
      
      @end
      

      //
      //  BackgroundSessionManager.m
      //
      //  Created by Robert Ryan on 10/11/14.
      //  Copyright (c) 2014 Robert Ryan. All rights reserved.
      //
      
      #import "BackgroundSessionManager.h"
      
      static NSString * const kBackgroundSessionIdentifier = @"com.domain.backgroundsession";
      
      @implementation BackgroundSessionManager
      
      + (instancetype)sharedManager {
          static id sharedMyManager = nil;
          static dispatch_once_t onceToken;
          dispatch_once(&onceToken, ^{
              sharedMyManager = [[self alloc] init];
          });
          return sharedMyManager;
      }
      
      - (instancetype)init {
          NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:kBackgroundSessionIdentifier];
          self = [super initWithSessionConfiguration:configuration];
          if (self) {
              [self configureDownloadFinished];            // when download done, save file
              [self configureBackgroundSessionFinished];   // when entire background session done, call completion handler
              [self configureAuthentication];              // my server uses authentication, so let's handle that; if you don't use authentication challenges, you can remove this
          }
          return self;
      }
      
      - (void)configureDownloadFinished {
          // just save the downloaded file to documents folder using filename from URL
      
          [self setDownloadTaskDidFinishDownloadingBlock:^NSURL *(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, NSURL *location) {
              if ([downloadTask.response isKindOfClass:[NSHTTPURLResponse class]]) {
                  NSInteger statusCode = [(NSHTTPURLResponse *)downloadTask.response statusCode];
                  if (statusCode != 200) {
                      // handle error here, e.g.
      
                      NSLog(@"%@ failed (statusCode = %ld)", [downloadTask.originalRequest.URL lastPathComponent], statusCode);
                      return nil;
                  }
              }
      
              NSString *filename      = [downloadTask.originalRequest.URL lastPathComponent];
              NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
              NSString *path          = [documentsPath stringByAppendingPathComponent:filename];
              return [NSURL fileURLWithPath:path];
          }];
      
          [self setTaskDidCompleteBlock:^(NSURLSession *session, NSURLSessionTask *task, NSError *error) {
              if (error) {
                  // handle error here, e.g.,
      
                  NSLog(@"%@: %@", [task.originalRequest.URL lastPathComponent], error);
              }
          }];
      }
      
      - (void)configureBackgroundSessionFinished {
          typeof(self) __weak weakSelf = self;
      
          [self setDidFinishEventsForBackgroundURLSessionBlock:^(NSURLSession *session) {
              if (weakSelf.savedCompletionHandler) {
                  weakSelf.savedCompletionHandler();
                  weakSelf.savedCompletionHandler = nil;
              }
          }];
      }
      
      - (void)configureAuthentication {
          NSURLCredential *myCredential = [NSURLCredential credentialWithUser:@"userid" password:@"password" persistence:NSURLCredentialPersistenceForSession];
      
          [self setTaskDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing *credential) {
              if (challenge.previousFailureCount == 0) {
                  *credential = myCredential;
                  return NSURLSessionAuthChallengeUseCredential;
              } else {
                  return NSURLSessionAuthChallengePerformDefaultHandling;
              }
          }];
      }
      
      @end
      
    2. 确保应用委托保存完成处理程序(根据需要实例化后台会话):

      - (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
          NSAssert([[BackgroundSessionManager sharedManager].session.configuration.identifier isEqualToString:identifier], @"Identifiers didn't match");
          [BackgroundSessionManager sharedManager].savedCompletionHandler = completionHandler;
      }
      
    3. 然后开始下载:

      for (NSString *filename in filenames) {
          NSURL *url = [baseURL URLByAppendingPathComponent:filename];
          NSURLRequest *request = [NSURLRequest requestWithURL:url];
          [[[BackgroundSessionManager sharedManager] downloadTaskWithRequest:request progress:nil destination:nil completionHandler:nil] resume];
      }
      

      注意,我不提供任何与任务相关的块,因为这些块在后台会话中不可靠。 (即使在应用程序终止并且这些块早已消失后,后台下载也会继续。)必须依赖会话级别,仅可轻松重新创建 setDownloadTaskDidFinishDownloadingBlock

    显然这是一个简单的例子(只有一个后台会话对象;只是使用 URL 的最后一个组件作为文件名将文件保存到 docs 文件夹等),但希望它说明了这种模式。

    【讨论】:

    • 我明白如何使用 iOS 7/8 中的基本 NSURLSession API 来做同样的事情。能否举个例子说明如何实现setDownloadTaskDidFinishDownloadingBlock: API?
    • @p0lAris 请参阅我修改后答案末尾的示例。
    • 谢谢罗伯。我有一个非常小的后续问题。我已经完成了上述操作(尽管方式不同)。我设置了核心数据模型,使图像是二进制数据(因为它们非常小)。我想在下载完成后说——someManagedObject.image = [NSData dataWithContentsOfFile:...];在哪里可以实现这一点?
    • 您也可以在 setDownloadTaskDidFinishDownloadingBlock 中执行此操作。或者你可能会让那个块发布一个通知,任何控制器正在处理托管对象上下文都会观察,然后在那个观察者那里做。
    • @MichalGumny 显然,不是在应用程序终止时。哈哈。但是如果用户碰巧在下载过程中重启了应用程序,只要你用相同的标识符重新实例化一个后台NSURLSession(即不要像我们通常那样依赖handleEventsForBackgroundURLSession来重新实例化后台会话),然后 didWriteData 委托方法将在数据继续进入时被调用。
    【解决方案2】:

    回调是否是块应该没有任何区别。当您实例化 AFURLSessionManager 时,请确保使用 NSURLSessionConfiguration backgroundSessionConfiguration: 实例化它。此外,请确保使用您的回调块调用经理的setDidFinishEventsForBackgroundURLSessionBlock - 这是您应该编写通常在 NSURLSessionDelegate 的方法中定义的代码的地方: URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session。此代码应调用您的应用委托的后台下载完成处理程序。

    关于后台下载任务的一点建议 - 即使在前台运行时,它们的超时也会被忽略,这意味着您可能会“卡住”没有响应的下载。这在任何地方都没有记录,让我发疯了一段时间。第一个嫌疑人是 AFNetworking,但即使在直接调用 NSURLSession 之后,行为仍然保持不变。

    祝你好运!

    【讨论】:

    • 嗯。你能澄清你所说的回调是否是块无关紧要的说法是什么意思吗?对于backgroundSessionConfiguration,您必须为您的后台会话指定委托。正如backgroundSessionConfiguration 文档所说,“后台传输有一些额外的限制,例如需要提供委托。”事实上,如果您尝试在后台会话中使用带有完成块的下载任务,您会收到 NSGenericException:“后台会话中不支持完成处理程序块。请改用委托。”
    • @Rob AFNetworking 会为您做到这一点 - 毕竟它是 iOS 上网络的便利包装器 - 当您通过 setDidFinishEventsForBackgroundURLSessionBlock 指定块时,AFNetworking 将自己注册为委托,以便一切工作它应该。
    • 感谢您的回答,但我不确定我是否明白我应该做什么。我在我的问题中添加了示例,如果可以,请看一下。
    • @Stavash 我花了很多时间研究这个问题,我对使用带有AFURLSessionManager 的块的担忧是有道理的。 NSURLSession 不允许基于块的便捷方法与后台会话是有原因的。当然,AFNetworking 使用委托方法,但这些方法调用会阻止应用程序作为参数传递给downloadTaskWithRequest。如果您在后台会话中使用AFURLSessionManager,并且应用程序终止,AFNetworking 不会正确处理此问题。我在 AFNetworking 上创建了an issue
    • 顺便说一句,我注意到 AFNetworking 类引用现在带有关于尝试将其中一些与任务相关的块与后台会话一起使用的明确、明确的警告,鼓励人们改用基于会话的块。我很高兴他们在这一点上改进了文档!
    【解决方案3】:

    AFURLSessionManager

    AFURLSessionManager基于指定的NSURLSessionConfiguration对象创建和管理NSURLSession对象,该对象符合<NSURLSessionTaskDelegate><NSURLSessionDataDelegate><NSURLSessionDownloadDelegate><NSURLSessionDelegate>

    链接到文档here documentation

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-18
      相关资源
      最近更新 更多