AFNetworking和后台传输

我有点混淆如何利用新的iOS 7 NSURLSession后台传输function和AFNetworking (版本2和3)。

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

这是通过使用新的API application:handleEventsForBackgroundURLSession:completionHandler:事实上,会话的委托最终将获得callback并可以完成其任务。

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

问题是,AFNetworking方便地使用基于块的API来做所有的请求,但是如果应用程序终止或崩溃,那些块也不见了。 那么我怎么才能完成这个任务?

或者也许我在这里错过了一些东西…

让我解释一下我的意思:

例如,我的应用程序是一个照片消息应用程序,可以说我有一个PhotoMessage对象代表一个消息,这个对象有像

  • state – 描述照片下载的状态。
  • resourcePath – 最终下载的照片文件的path。

所以当我从服务器得到一条新消息时,我创build了一个新的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

一些想法:

  1. 您必须确保您执行URL加载系统编程指南的处理iOS背景活动部分中所述的必要编码,并说:

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

    该指南展示了你可以做的一些事例。 坦率地说,我认为在WWDC 2013video的后半部分讨论的代码示例更加清晰。

  2. 如果应用程序仅仅是暂停的, AFURLSessionManager的基本实现将与后台会话一起工作(假如你已经完成了上述工作,你会在networking任务完成时看到你的块被调用)。 但是正如你所猜测的那样,传递给AFURLSessionManager方法的任何特定于块的参数都会丢失,如果应用程序终止或崩溃,则创build用于上传和下载的NSURLSessionTask

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

    事实certificate,这个丢失块的问题实际上对于后台下载来说更成问题,但是这里的解决scheme非常相似(不要使用基于任务的块参数,而是使用基于会话的块,比如setDownloadTaskDidFinishDownloadingBlock )。

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

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

    但是请确保networking任务的完成块正确地通知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, eg 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, eg, 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]; } 

    请注意,我不提供任何与任务有关的块,因为这些块与后台会话不可靠。 (甚至在应用程序被终止后,后台下载也会继续进行,而这些块已经消失了。)必须依赖会话级别,只需轻松地重新创buildsetDownloadTaskDidFinishDownloadingBlock

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

不pipecallback是否阻止,都不会有什么区别。 当你实例化一个AFURLSessionManager ,一定要用NSURLSessionConfiguration backgroundSessionConfiguration:实例化NSURLSessionConfiguration backgroundSessionConfiguration: 此外,请务必使用callback块调用pipe理器的setDidFinishEventsForBackgroundURLSessionBlock – 这是您应该编写通常在NSURLSessionDelegate方法中定义的代码的位置: URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session 。 此代码应该调用您的应用程序委托的后台下载完成处理程序。

关于后台下载任务的一个build议是,即使在前台运行时,他们的超时也会被忽略,这意味着你可能会在没有响应的下载中“卡住”。 这没有logging在任何地方,使我疯狂了一段时间。 第一个嫌疑人是AFNetworking,但即使直接调用NSURLSession,行为仍然是一样的。

祝你好运!

AFURLSessionManager

AFURLSessionManager根据符合<NSURLSessionTaskDelegate><NSURLSessionDataDelegate><NSURLSessionDownloadDelegate><NSURLSessionDelegate>的指定NSURLSessionConfiguration对象创build和pipe理NSURLSession对象。

链接到文档这里的文档