web-dev-qa-db-fra.com

AFRéseautage et transferts en arrière-plan

Je suis un peu confus sur la façon de profiter des nouvelles fonctionnalités de transfert d'arrière-plan iOS 7 NSURLSession et AFNetworking (versions 2 et 3).

J'ai vu le WWDC 705 - What’s New in Foundation Networking session, et ils ont démontré le téléchargement en arrière-plan qui continue après la fin de l'application ou même un plantage.

Cela se fait à l'aide de la nouvelle API application:handleEventsForBackgroundURLSession:completionHandler: et le fait que le délégué de la session finira par recevoir les rappels et pourra terminer sa tâche.

Je me demande donc comment l'utiliser avec AFNetworking (si possible) pour continuer le téléchargement en arrière-plan.

Le problème est que AFNetworking utilise commodément l'API basée sur des blocs pour effectuer toutes les demandes, mais si l'application s'est arrêtée ou se bloque, ces blocs ont également disparu. Alors, comment puis-je terminer la tâche?

Ou peut-être qu'il me manque quelque chose ici ...

Permettez-moi d'expliquer ce que je veux dire:

Par exemple, mon application est une application de messagerie photo, disons que j'ai un objet PhotoMessage qui représente un message et cet objet a des propriétés comme

  • state - décrit l'état du téléchargement des photos.
  • resourcePath - le chemin vers le fichier photo téléchargé final.

Ainsi, lorsque je reçois un nouveau message du serveur, je crée un nouvel objet PhotoMessage et commence à télécharger sa ressource photo.

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];   

Comme vous pouvez le voir, j'utilise le bloc d'achèvement pour mettre à jour cet objet PhotoMessage en fonction de la réponse que j'obtiens.

Comment puis-je accomplir cela avec un transfert en arrière-plan? Ce bloc d'achèvement ne sera pas appelé et par conséquent, je ne peux pas mettre à jour le newPhotoMsg.

52
Mario

Quelques réflexions:

  1. Vous devez vous assurer que vous effectuez le codage nécessaire décrit dans la section Gestion de l'activité en arrière-plan iOS du Guide de programmation du système de chargement d'URL dit:

    Si vous utilisez NSURLSession dans iOS, votre application est automatiquement relancée à la fin d'un téléchargement. La méthode de délégué d'application application:handleEventsForBackgroundURLSession:completionHandler: De votre application est chargée de recréer la session appropriée, de stocker un gestionnaire d'achèvement et d'appeler ce gestionnaire lorsque la session appelle la méthode URLSessionDidFinishEventsForBackgroundURLSession: De votre délégué de session.

    Ce guide montre quelques exemples de ce que vous pouvez faire. Franchement, je pense que les exemples de code discutés dans la dernière partie de la vidéo WWDC 2013 Quoi de neuf dans Foundation Networking sont encore plus clairs.

  2. L'implémentation de base de AFURLSessionManager fonctionnera en conjonction avec les sessions d'arrière-plan si l'application est simplement suspendue (vous verrez vos blocs appelés lorsque les tâches réseau sont terminées, en supposant que vous avez fait ce qui précède). Mais comme vous l'avez deviné, tous les paramètres de bloc spécifiques à une tâche qui sont passés à la méthode AFURLSessionManager où vous créez le NSURLSessionTask pour les chargements et les téléchargements sont perdus "si l'application se termine ou se bloque".

    Pour les téléchargements en arrière-plan, il s'agit d'une gêne (car les blocs de progression et d'achèvement des informations au niveau de la tâche que vous avez spécifiés lors de la création de la tâche ne seront pas appelés). Mais si vous utilisez les rendus au niveau de la session (par exemple setTaskDidCompleteBlock et setTaskDidSendBodyDataBlock), cela sera appelé correctement (en supposant que vous définissez toujours ces blocs lorsque vous réinstanciez le gestionnaire de session).

    En fait, ce problème de perte des blocs est en fait plus problématique pour les téléchargements en arrière-plan, mais la solution est très similaire (n'utilisez pas les paramètres de bloc basés sur les tâches, mais utilisez plutôt des blocs basés sur la session, tels que setDownloadTaskDidFinishDownloadingBlock).

  3. Une alternative, vous pouvez vous en tenir à la valeur par défaut (non en arrière-plan) NSURLSession, mais assurez-vous que votre application demande un peu de temps pour terminer le téléchargement si l'utilisateur quitte l'application pendant que la tâche est en cours. Par exemple, avant de créer votre NSURLSessionTask, vous pouvez créer un UIBackgroundTaskIdentifier:

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

    Mais assurez-vous que le bloc d'achèvement de la tâche réseau informe correctement iOS qu'il est terminé:

    if (taskId != UIBackgroundTaskInvalid) {
        [[UIApplication sharedApplication] endBackgroundTask:taskId];
        taskId = UIBackgroundTaskInvalid;
    }
    

    Ce n'est pas aussi puissant qu'un arrière-plan NSURLSession (par exemple, vous disposez d'un temps limité), mais dans certains cas, cela peut être utile.


Mise à jour:

J'ai pensé ajouter un exemple pratique de la façon de faire des téléchargements en arrière-plan à l'aide d'AFNetworking.

  1. Définissez d'abord votre gestionnaire d'arrière-plan.

    //
    //  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
    

    et

    //
    //  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
    
  2. Assurez-vous que le délégué d'application enregistre le gestionnaire d'achèvement (en instanciant la session d'arrière-plan si nécessaire):

    - (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. Commencez ensuite vos téléchargements:

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

    Remarque, je ne fournis aucun de ces blocs liés aux tâches, car ceux-ci ne sont pas fiables avec les sessions d'arrière-plan. (Les téléchargements en arrière-plan se poursuivent même après la fin de l'application et ces blocs ont disparu depuis longtemps.) Il faut se fier uniquement au niveau de la session, facilement recréé setDownloadTaskDidFinishDownloadingBlock.

Il s'agit clairement d'un exemple simple (un seul objet de session d'arrière-plan; il suffit d'enregistrer des fichiers dans le dossier docs en utilisant le dernier composant de l'URL comme nom de fichier; etc.), mais j'espère qu'il illustre le modèle.

77
Rob

Cela ne devrait faire aucune différence que les rappels soient des blocs ou non. Lorsque vous instanciez un AFURLSessionManager, assurez-vous de l'instancier avec NSURLSessionConfiguration backgroundSessionConfiguration:. Assurez-vous également d'appeler le setDidFinishEventsForBackgroundURLSessionBlock du gestionnaire avec votre bloc de rappel - c'est là que vous devez écrire le code généralement défini dans la méthode de NSURLSessionDelegate: URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session. Ce code doit appeler le gestionnaire de fin de téléchargement en arrière-plan de votre délégué d'application.

Un mot de conseils concernant les tâches de téléchargement en arrière-plan - même lors de l'exécution au premier plan, leurs délais d'attente sont ignorés, ce qui signifie que vous pourriez être "bloqué" sur un téléchargement qui ne répond pas. Cela n'est documenté nulle part et m'a rendu fou pendant un certain temps. Le premier suspect était AFNetworking mais même après avoir appelé NSURLSession directement, le comportement est resté le même.

Bonne chance!

2
Stavash