AFNetworking y trasfondo de transferencias

Estoy un poco confundido de cómo aprovechar las nuevas características de transferencia de fondo de iOS 7 NSURLSession y AFNetworking (versiones 2 y 3).

Vi la WWDC 705 - What's New in Foundation Networking sesión de WWDC 705 - What's New in Foundation Networking , y demostraron la descarga de fondo que continúa después de que la aplicación finaliza o incluso se cuelga.

Esto se hace usando la nueva application:handleEventsForBackgroundURLSession:completionHandler: API application:handleEventsForBackgroundURLSession:completionHandler: y el hecho de que el delegado de la sesión eventualmente obtenga las devoluciones de llamada y pueda completar su tarea.

Así que me pregunto cómo usarlo con AFNetworking (si es posible) para continuar descargando en segundo plano.

El problema es que AFNetworking utiliza convenientemente API basada en bloques para hacer todas las solicitudes, pero si la aplicación finaliza o bloquea esos bloques también desaparecen. Entonces, ¿cómo puedo completar la tarea?

O tal vez me falta algo aquí …

Déjame explicar lo que quiero decir:

Por ejemplo, mi aplicación es una aplicación de mensajería de fotos, digamos que tengo un objeto PhotoMessage que representa un mensaje y este objeto tiene propiedades como

  • state : describe el estado de la descarga de la foto.
  • resourcePath : la ruta al archivo final de la foto descargada.

Entonces, cuando recibo un mensaje nuevo del servidor, creo un nuevo objeto PhotoMessage y empiezo a descargar su recurso fotográfico.

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

Como puede ver, utilizo el bloque de finalización para actualizar ese objeto PhotoMessage acuerdo con la respuesta que recibo.

¿Cómo puedo lograr eso con una transferencia de fondo? No se llamará a este bloque de finalización y, como resultado, no puedo actualizar el nuevo newPhotoMsg .

Un par de pensamientos:

  1. Debe asegurarse de que realiza la encoding necesaria descrita en la sección Actividad de fondo de iOS de manejo de la guía de progtwigción del sistema de carga de URL, que dice:

    Si está utilizando NSURLSession en iOS, su aplicación se relanza automáticamente cuando finaliza una descarga. La aplicación de su application:handleEventsForBackgroundURLSession:completionHandler: método de delegado de la aplicación es responsable de application:handleEventsForBackgroundURLSession:completionHandler: la sesión apropiada, almacenar un controlador de finalización y llamar a ese controlador cuando la sesión llama al método URLSessionDidFinishEventsForBackgroundURLSession: su delegado de URLSessionDidFinishEventsForBackgroundURLSession: .

    Esa guía muestra algunos ejemplos de lo que puedes hacer. Francamente, creo que los ejemplos del código discutidos en la última parte del video de WWDC 2013 Las Novedades en la creación de redes de fundaciones son aún más claros.

  2. La implementación básica de AFURLSessionManager funcionará junto con las sesiones en segundo plano si la aplicación simplemente se suspende (verá sus bloques llamados cuando las tareas de la red estén completas, suponiendo que haya hecho lo anterior). Pero como ya adivinó, cualquier parámetro de bloque específico de la tarea que se pase al método AFURLSessionManager donde se crea NSURLSessionTask para las cargas y descargas se pierde “si la aplicación finaliza o se bloquea”.

    Para las cargas en segundo plano, esto es una molestia (ya que no se llamará a los bloques de progreso y conclusión informativos a nivel de tarea que especificó al crear la tarea). Pero si emplea las representaciones a nivel de sesión (por ejemplo, setTaskDidCompleteBlock y setTaskDidSendBodyDataBlock ), se llamará correctamente (suponiendo que siempre establezca estos bloques cuando vuelva a crear instancias del administrador de sesión).

    Como resultado, esta cuestión de perder los bloques es realmente más problemática para las descargas en segundo plano, pero la solución es muy similar (no use parámetros de bloques basados ​​en tareas, sino más bien use bloques basados ​​en sesiones, como setDownloadTaskDidFinishDownloadingBlock ).

  3. Como alternativa, puede quedarse con NSURLSession predeterminado (sin antecedentes), pero asegúrese de que su aplicación solicite un poco de tiempo para finalizar la carga si el usuario abandona la aplicación mientras la tarea está en progreso. Por ejemplo, antes de crear su NSURLSessionTask , puede crear un UIBackgroundTaskIdentifier :

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

    Pero asegúrese de que el bloque de finalización de la tarea de red informe correctamente a iOS que está completo:

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

    Esto no es tan poderoso como un NSURLSession fondo (por ejemplo, tiene una cantidad limitada de tiempo disponible), pero en algunos casos esto puede ser útil.


Actualizar:

Pensé que agregaría un ejemplo práctico de cómo hacer descargas de fondo usando AFNetworking.

  1. Primero defina su administrador de fondo.

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

    y

     // // 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. Asegúrese de que el delegado de la aplicación guarda el controlador de finalización (instanciando la sesión de fondo según sea necesario):

     - (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. Luego comienza tus descargas:

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

    Tenga en cuenta que no proporciono ninguno de esos bloques relacionados con tareas, porque no son confiables con las sesiones en segundo plano. (Las descargas en segundo plano continúan incluso después de que la aplicación finalice y estos bloques hayan desaparecido por mucho tiempo). Se debe confiar solo en el setDownloadTaskDidFinishDownloadingBlock nivel de sesión, que se recreó fácilmente.

Claramente, este es un ejemplo simple (solo un objeto de sesión en segundo plano, simplemente guardando archivos en la carpeta de documentos usando el último componente de la URL como nombre de archivo; etc.), pero con suerte ilustra el patrón.

No debería hacer ninguna diferencia si las devoluciones de llamada son bloques o no. Cuando AFURLSessionManager una instancia de AFURLSessionManager , asegúrese de crear una instancia con NSURLSessionConfiguration backgroundSessionConfiguration: Además, asegúrese de llamar a setDidFinishEventsForBackgroundURLSessionBlock del setDidFinishEventsForBackgroundURLSessionBlock con su locking de callback: aquí es donde debe escribir el código definido normalmente en la sesión del método URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session . Este código debe invocar el manejador de finalización de descarga de fondo del delegado de su aplicación.

Un consejo sobre las tareas de descarga en segundo plano: incluso cuando se ejecuta en primer plano, se ignoran sus tiempos de espera, lo que significa que puede quedar “atascado” en una descarga que no responde. Esto no está documentado en ninguna parte y me volvió loco por algún tiempo. El primer sospechoso era AFNetworking, pero incluso después de llamar directamente a NSURLSession, el comportamiento seguía siendo el mismo.

¡Buena suerte!

AFURLSessionManager

AFURLSessionManager crea y administra un objeto NSURLSession basado en un objeto NSURLSessionConfiguration especificado, que se ajusta a , , y .

enlace a documentación aquí documentación