AFNetworking и фоновые передачи
Я немного запутался, как воспользоваться новой 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 ответа
Пара мыслей:
Вы должны убедиться, что вы выполняете необходимое кодирование, описанное в разделе " Обработка фоновой активности iOS " в Руководстве по программированию системы загрузки URL-адресов:
Если вы используете
NSURLSession
в iOS ваше приложение автоматически перезапускается после завершения загрузки. Ваше приложениеapplication:handleEventsForBackgroundURLSession:completionHandler:
Метод делегата приложения отвечает за воссоздание соответствующего сеанса, сохранение обработчика завершения и вызов этого обработчика, когда сеанс вызывает делегата сеанса.URLSessionDidFinishEventsForBackgroundURLSession:
метод.Это руководство показывает несколько примеров того, что вы можете сделать. Честно говоря, я думаю, что примеры кода, обсуждаемые в последней части видео WWDC 2013 " Что нового в Foundation Networking", еще более понятны.
Основная реализация
AFURLSessionManager
будет работать в сочетании с фоновыми сессиями, если приложение просто приостановлено (вы увидите, что ваши блоки вызываются, когда сетевые задачи выполнены, если вы уже сделали выше). Но, как вы уже догадались, любые специфические для задачи параметры блока, которые передаютсяAFURLSessionManager
метод, в котором вы создаетеNSURLSessionTask
для загрузки и скачивания теряются "если приложение закрывается или вылетает".Для фоновых загрузок это раздражает (поскольку ваши информационные блоки выполнения и завершения на уровне задачи, которые вы указали при создании задачи, не будут вызываться). Но если вы используете представления сеансового уровня (например,
setTaskDidCompleteBlock
а такжеsetTaskDidSendBodyDataBlock
), который будет вызываться правильно (при условии, что вы всегда устанавливаете эти блоки при повторном создании менеджера сеансов).Как оказалось, эта проблема потери блоков на самом деле более проблематична для фоновых загрузок, но решение там очень похожее (не используйте параметры блоков на основе задач, а скорее используйте блоки на основе сеансов, такие как
setDownloadTaskDidFinishDownloadingBlock
).Альтернатива, вы можете придерживаться по умолчанию (без фона)
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.
Сначала определите свой фоновый менеджер.
// // 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
Убедитесь, что делегат приложения сохраняет обработчик завершения (при необходимости создаёт фоновый сеанс):
- (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]; }
Обратите внимание, я не предоставляю ни одного из этих блоков, связанных с задачей, потому что они не надежны с фоновыми сессиями. (Фоновые загрузки продолжаются даже после завершения работы приложения, и эти блоки давно исчезли.) Необходимо полагаться на легко воссозданный на уровне сеанса
setDownloadTaskDidFinishDownloadingBlock
только.
Понятно, что это простой пример (только один фоновый объект сеанса; просто сохранение файлов в папке docs с использованием последнего компонента URL в качестве имени файла и т. Д.), Но, надеюсь, он иллюстрирует шаблон.
Не должно иметь никакого значения, являются ли обратные вызовы блоками или нет. Когда вы создаете экземпляр AFURLSessionManager
убедитесь, что создали его NSURLSessionConfiguration backgroundSessionConfiguration:
, Также обязательно позвоните менеджеру setDidFinishEventsForBackgroundURLSessionBlock
с вашим блоком обратного вызова - здесь вы должны написать код, обычно определенный в методе NSURLSessionDelegate:URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
, Этот код должен вызывать обработчик завершения загрузки фонового делегата вашего приложения.
Один совет относительно задач фоновой загрузки - даже при запуске на переднем плане их таймауты игнорируются, что означает, что вы можете "застрять" при загрузке, которая не отвечает. Это нигде не задокументировано и на какое-то время сводило меня с ума. Первым подозреваемым был AFNetworking, но даже после прямого вызова NSURLSession поведение оставалось прежним.
Удачи!
AFURLSessionManager
AFURLSessionManager
создает и управляет NSURLSession
объект на основе указанного NSURLSessionConfiguration
объект, который соответствует <NSURLSessionTaskDelegate>
, <NSURLSessionDataDelegate>
, <NSURLSessionDownloadDelegate>
, а также <NSURLSessionDelegate>
,
ссылка на документацию здесь документация