ios - afhttprequestoperation - AFNetworking y trasfondo de transferencias
afnetworking vs alamofire (3)
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
.
AFURLSessionManager
AFURLSessionManager
crea y administra un objeto NSURLSession
basado en un objeto NSURLSessionConfiguration
especificado, que se ajusta a <NSURLSessionTaskDelegate>
, <NSURLSessionDataDelegate>
, <NSURLSessionDownloadDelegate>
y <NSURLSessionDelegate>
.
enlace a documentación aquí documentació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 bloqueo de devolución de llamada: 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!
Un par de pensamientos:
Debe asegurarse de que realiza la codificación necesaria descrita en la sección Actividad de fondo de iOS de manejo de la guía de programació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 suapplication:handleEventsForBackgroundURLSession:completionHandler:
método de delegado de la aplicación es responsable deapplication:handleEventsForBackgroundURLSession:completionHandler:
la sesión apropiada, almacenar un controlador de finalización y llamar a ese controlador cuando la sesión llama al métodoURLSessionDidFinishEventsForBackgroundURLSession:
su delegado deURLSessionDidFinishEventsForBackgroundURLSession:
.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.
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étodoAFURLSessionManager
donde se creaNSURLSessionTask
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
ysetTaskDidSendBodyDataBlock
), 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
).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 suNSURLSessionTask
, puede crear unUIBackgroundTaskIdentifier
: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.
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, 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
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; }
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.