AFNetworking和后台转移

时间:2014-01-25 11:48:28

标签: ios objective-c afnetworking afnetworking-2

我对如何利用新的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

3 个答案:

答案 0 :(得分:76)

有几点想法:

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

      

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

    该指南展示了您可以做的一些示例。坦率地说,我认为WWDC 2013视频What’s New in Foundation Networking后半部分讨论的代码示例更加清晰。

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

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

    事实证明,丢失块的这个问题实际上对于后台下载来说更成问题,但是那里的解决方案非常相似(不要使用基于任务的块参数,而是使用基于会话的块,例如{ {1}})。

  3. 另一种选择,您可以坚持使用默认(非背景)setDownloadTaskDidFinishDownloadingBlock,但如果用户在任务进行过程中离开应用,请确保您的应用请求一点时间完成上传。例如,在创建NSURLSession之前,您可以创建NSURLSessionTask

    UIBackgroundTaskIdentifier

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

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

    这不如后台if (taskId != UIBackgroundTaskInvalid) { [[UIApplication sharedApplication] endBackgroundTask:taskId]; taskId = UIBackgroundTaskInvalid; } 强大(例如,您的时间有限),但在某些情况下,这可能很有用。


  4. <强>更新

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

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

      NSURLSession

      //
      //  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
      
    2. 确保app delegate保存完成处理程序(根据需要实例化后台会话):

      //
      //  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
      
    3. 然后开始下载:

      - (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;
      }
      

      注意,我不提供任何与任务相关的块,因为这些块在后台会话中不可靠。 (即使在应用程序终止并且这些块已经很久消失之后,后台下载仍会继续。)必须依赖会话级别,只能轻松重新创建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]; }

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

答案 1 :(得分:2)

回调是否为阻塞应该没有任何区别。实例化AFURLSessionManager时,请确保使用NSURLSessionConfiguration backgroundSessionConfiguration:对其进行实例化。另外,确保使用回调块调用管理器setDidFinishEventsForBackgroundURLSessionBlock - 这是您应该编写通常在NSURLSessionDelegate方法中定义的代码的地方: URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session。此代码应调用您的app delegate的后台下载完成处理程序。

关于后台下载任务的一个建议 - 即使在前台运行时,它们的超时也会被忽略,这意味着你可能会“陷入”没有响应的下载。这在任何地方都没有记录,并且让我疯狂了一段时间。第一个嫌疑人是AFNetworking,但即使直接致电NSURLSession,行为也保持不变。

祝你好运!

答案 2 :(得分:-3)

AFURLSessionManager

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

链接到文档here documentation