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
。
一些想法:
-
您必须确保您执行URL加载系统编程指南的处理iOS背景活动部分中所述的必要编码,并说:
如果您在iOS中使用
NSURLSession
,则在下载完成时,您的应用程序将自动重新启动。 您的应用程序的application:handleEventsForBackgroundURLSession:completionHandler:
应用程序委托方法负责重新创build适当的会话,存储完成处理程序,并在会话调用您的会话委托的URLSessionDidFinishEventsForBackgroundURLSession:
方法时调用该处理程序。该指南展示了你可以做的一些事例。 坦率地说,我认为在WWDC 2013video的后半部分讨论的代码示例更加清晰。
-
如果应用程序仅仅是暂停的,
AFURLSessionManager
的基本实现将与后台会话一起工作(假如你已经完成了上述工作,你会在networking任务完成时看到你的块被调用)。 但是正如你所猜测的那样,传递给AFURLSessionManager
方法的任何特定于块的参数都会丢失,如果应用程序终止或崩溃,则创build用于上传和下载的NSURLSessionTask
。对于后台上传,这是一个烦恼(因为您创build任务时指定的任务级信息进度和完成块不会被调用)。 但是,如果使用会话级别的翻译(例如,
setTaskDidCompleteBlock
和setTaskDidSendBodyDataBlock
),则会正确调用(假设您在重新实例化会话pipe理器时始终设置这些块)。事实certificate,这个丢失块的问题实际上对于后台下载来说更成问题,但是这里的解决scheme非常相似(不要使用基于任务的块参数,而是使用基于会话的块,比如
setDownloadTaskDidFinishDownloadingBlock
)。 -
或者,您可以坚持使用默认(非后台)
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做后台下载的实际例子。
-
首先定义你的背景经理。
// // 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
-
确保应用程序委托保存完成处理程序(根据需要实例化后台会话):
- (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]; }
请注意,我不提供任何与任务有关的块,因为这些块与后台会话不可靠。 (甚至在应用程序被终止后,后台下载也会继续进行,而这些块已经消失了。)必须依赖会话级别,只需轻松地重新创build
setDownloadTaskDidFinishDownloadingBlock
。
显然,这是一个简单的例子(只有一个后台会话对象,只是使用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
对象。
链接到文档这里的文档