Подтвердить что ты не робот

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.

4b9b3361

Ответ 1

Несколько мыслей:

  • Вы должны убедиться, что вы сделали необходимую кодировку, описанную в разделе "Обработка фоновой активности iOS" в разделе "Загрузка системы загрузки URL" Руководство говорит:

    Если вы используете NSURLSession в iOS, ваше приложение автоматически перезапускается при завершении загрузки. Ваш метод приложений app 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 (например, у вас есть ограниченное количество времени), но в некоторых случаях это может быть полезно.


Update:

Я думал, что добавлю практический пример того, как делать фоновые загрузки, используя 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 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.

Очевидно, что это простой пример (только один фоновый объект сеанса, просто сохранение файлов в папку документов с использованием последнего компонента URL как имя файла и т.д.), но, надеюсь, он иллюстрирует шаблон.

Ответ 2

Не нужно иметь значения, являются ли обратные вызовы блоками или нет. Когда вы создаете экземпляр AFURLSessionManager, обязательно создайте его с помощью NSURLSessionConfiguration backgroundSessionConfiguration:. Кроме того, не забудьте вызвать менеджера setDidFinishEventsForBackgroundURLSessionBlock с помощью блока обратного вызова - здесь вы должны написать код, обычно определенный в методе NSURLSessionDelegate: URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session. Этот код должен вызывать обработчик завершения загрузки приложения делегата приложения.

Одно из советов относительно задач загрузки фона - даже при запуске на переднем плане их таймауты игнорируются, что означает, что вы можете "застревать" при загрузке, которая не отвечает. Это нигде не документировано, и я некоторое время сбивал с ума. Первым подозреваемым было AFNetworking, но даже после прямого вызова NSURLSession поведение оставалось прежним.

Удачи!

Ответ 3

AFURLSessionManager

AFURLSessionManager создает и управляет объектом NSURLSession на основе указанного объекта NSURLSessionConfiguration, который соответствует <NSURLSessionTaskDelegate>, <NSURLSessionDataDelegate>, <NSURLSessionDownloadDelegate> и <NSURLSessionDelegate>.

ссылка на документацию здесь документация