ios objective-c network-programming multitasking background-thread

ios - objetivo c-Uso adecuado de beginBackgroundTaskWithExpirationHandler



objective-c network-programming (5)

Estoy un poco confundido sobre cómo y cuándo usar beginBackgroundTaskWithExpirationHandler .

Apple muestra en sus ejemplos para usarlo en el delegado applicationDidEnterBackground , para obtener más tiempo para completar una tarea importante, generalmente una transacción de red.

Al buscar en mi aplicación, parece que la mayoría de los elementos de mi red son importantes, y cuando uno comienza, me gustaría completarlo si el usuario presiona el botón de inicio.

Entonces, ¿es una buena práctica aceptar todas las transacciones de red (y no estoy hablando de descargar grandes cantidades de datos, en su mayor parte, un xml corto) con beginBackgroundTaskWithExpirationHandler para estar seguro?


  • (void) doUpdate {dispatch_async (dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ {

    [self beginBackgroundUpdateTask]; NSURLResponse * response = nil; NSError * error = nil; NSData * responseData = [NSURLConnection sendSynchronousRequest: request returningResponse: &response error:

    &error];

    // Do something with the result [self endBackgroundUpdateTask];

    }); }

  • (void) beginBackgroundUpdateTask {self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler: ^ {[self endBackgroundUpdateTask]; }]; }

  • (void) endBackgroundUpdateTask {[[UIApplication sharedApplication] endBackgroundTask: self.backgroundUpdateTask]; self.backgroundUpdateTask = UIBackgroundTaskInvalid; }

Gracias Ashley Mills, es un trabajo perfecto para mí


Aquí hay una clase Swift que encapsula la ejecución de una tarea en segundo plano:

class BackgroundTask { private let application: UIApplication private var identifier = UIBackgroundTaskInvalid init(application: UIApplication) { self.application = application } class func run(application: UIApplication, handler: (BackgroundTask) -> ()) { // NOTE: The handler must call end() when it is done let backgroundTask = BackgroundTask(application: application) backgroundTask.begin() handler(backgroundTask) } func begin() { self.identifier = application.beginBackgroundTaskWithExpirationHandler { self.end() } } func end() { if (identifier != UIBackgroundTaskInvalid) { application.endBackgroundTask(identifier) } identifier = UIBackgroundTaskInvalid } }

La forma más sencilla de usarlo:

BackgroundTask.run(application) { backgroundTask in // Do something backgroundTask.end() }

Si necesita esperar la devolución de llamada de un delegado antes de finalizar, utilice algo como esto:

class MyClass { backgroundTask: BackgroundTask? func doSomething() { backgroundTask = BackgroundTask(application) backgroundTask!.begin() // Do something that waits for callback } func callback() { backgroundTask?.end() backgroundTask = nil } }


Implementé la solución de Joel. Aquí está el código completo:

.h archivo:

#import <Foundation/Foundation.h> @interface VMKBackgroundTaskManager : NSObject + (id) sharedTasks; - (NSUInteger)beginTask; - (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion; - (void)endTaskWithKey:(NSUInteger)_key; @end

archivo .m:

#import "VMKBackgroundTaskManager.h" @interface VMKBackgroundTaskManager() @property NSUInteger taskKeyCounter; @property NSMutableDictionary *dictTaskIdentifiers; @property NSMutableDictionary *dictTaskCompletionBlocks; @end @implementation VMKBackgroundTaskManager + (id)sharedTasks { static VMKBackgroundTaskManager *sharedTasks = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedTasks = [[self alloc] init]; }); return sharedTasks; } - (id)init { self = [super init]; if (self) { [self setTaskKeyCounter:0]; [self setDictTaskIdentifiers:[NSMutableDictionary dictionary]]; [self setDictTaskCompletionBlocks:[NSMutableDictionary dictionary]]; } return self; } - (NSUInteger)beginTask { return [self beginTaskWithCompletionHandler:nil]; } - (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion; { //read the counter and increment it NSUInteger taskKey; @synchronized(self) { taskKey = self.taskKeyCounter; self.taskKeyCounter++; } //tell the OS to start a task that should continue in the background if needed NSUInteger taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ [self endTaskWithKey:taskKey]; }]; //add this task identifier to the active task dictionary [self.dictTaskIdentifiers setObject:[NSNumber numberWithUnsignedLong:taskId] forKey:[NSNumber numberWithUnsignedLong:taskKey]]; //store the completion block (if any) if (_completion) [self.dictTaskCompletionBlocks setObject:_completion forKey:[NSNumber numberWithUnsignedLong:taskKey]]; //return the dictionary key return taskKey; } - (void)endTaskWithKey:(NSUInteger)_key { @synchronized(self.dictTaskCompletionBlocks) { //see if this task has a completion block CompletionBlock completion = [self.dictTaskCompletionBlocks objectForKey:[NSNumber numberWithUnsignedLong:_key]]; if (completion) { //run the completion block and remove it from the completion block dictionary completion(); [self.dictTaskCompletionBlocks removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]]; } } @synchronized(self.dictTaskIdentifiers) { //see if this task has been ended yet NSNumber *taskId = [self.dictTaskIdentifiers objectForKey:[NSNumber numberWithUnsignedLong:_key]]; if (taskId) { //end the task and remove it from the active task dictionary [[UIApplication sharedApplication] endBackgroundTask:[taskId unsignedLongValue]]; [self.dictTaskIdentifiers removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]]; NSLog(@"Task ended"); } } } @end


La respuesta aceptada es muy útil y debería estar bien en la mayoría de los casos, sin embargo, dos cosas me molestaron al respecto:

  1. Como señalaron varias personas, almacenar el identificador de tarea como una propiedad significa que se puede sobrescribir si se llama al método varias veces, lo que lleva a una tarea que nunca finalizará con gracia hasta que el SO la obligue a finalizar en el momento de su vencimiento. .

  2. Este patrón requiere una propiedad única para cada llamada a beginBackgroundTaskWithExpirationHandler que parece engorroso si tiene una aplicación más grande con muchos métodos de red.

Para resolver estos problemas, escribí un singleton que se encarga de todas las tuberías y rastrea las tareas activas en un diccionario. No se necesitan propiedades para realizar un seguimiento de los identificadores de tareas. Parece que funciona bien. El uso se simplifica a:

//start the task NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTask]; //do stuff //end the task [[BackgroundTaskManager sharedTasks] endTaskWithKey:taskKey];

Opcionalmente, si desea proporcionar un bloque de finalización que hace algo más allá de finalizar la tarea (que está integrado) puede llamar:

NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTaskWithCompletionHandler:^{ //do stuff }];

El código fuente relevante está disponible a continuación (solo se excluyen las cosas para abreviar). Comentarios / comentarios bienvenidos.

- (id)init { self = [super init]; if (self) { [self setTaskKeyCounter:0]; [self setDictTaskIdentifiers:[NSMutableDictionary dictionary]]; [self setDictTaskCompletionBlocks:[NSMutableDictionary dictionary]]; } return self; } - (NSUInteger)beginTask { return [self beginTaskWithCompletionHandler:nil]; } - (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion; { //read the counter and increment it NSUInteger taskKey; @synchronized(self) { taskKey = self.taskKeyCounter; self.taskKeyCounter++; } //tell the OS to start a task that should continue in the background if needed NSUInteger taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ [self endTaskWithKey:taskKey]; }]; //add this task identifier to the active task dictionary [self.dictTaskIdentifiers setObject:[NSNumber numberWithUnsignedLong:taskId] forKey:[NSNumber numberWithUnsignedLong:taskKey]]; //store the completion block (if any) if (_completion) [self.dictTaskCompletionBlocks setObject:_completion forKey:[NSNumber numberWithUnsignedLong:taskKey]]; //return the dictionary key return taskKey; } - (void)endTaskWithKey:(NSUInteger)_key { @synchronized(self.dictTaskCompletionBlocks) { //see if this task has a completion block CompletionBlock completion = [self.dictTaskCompletionBlocks objectForKey:[NSNumber numberWithUnsignedLong:_key]]; if (completion) { //run the completion block and remove it from the completion block dictionary completion(); [self.dictTaskCompletionBlocks removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]]; } } @synchronized(self.dictTaskIdentifiers) { //see if this task has been ended yet NSNumber *taskId = [self.dictTaskIdentifiers objectForKey:[NSNumber numberWithUnsignedLong:_key]]; if (taskId) { //end the task and remove it from the active task dictionary [[UIApplication sharedApplication] endBackgroundTask:[taskId unsignedLongValue]]; [self.dictTaskIdentifiers removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]]; } } }


Si desea que su transacción de red continúe en segundo plano, deberá envolverla en una tarea en segundo plano. También es muy importante que llame a endBackgroundTask cuando haya terminado; de lo contrario, la aplicación se endBackgroundTask vez que haya expirado el tiempo asignado.

Los míos tienden a tener un aspecto como este:

- (void) doUpdate { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self beginBackgroundUpdateTask]; NSURLResponse * response = nil; NSError * error = nil; NSData * responseData = [NSURLConnection sendSynchronousRequest: request returningResponse: &response error: &error]; // Do something with the result [self endBackgroundUpdateTask]; }); } - (void) beginBackgroundUpdateTask { self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ [self endBackgroundUpdateTask]; }]; } - (void) endBackgroundUpdateTask { [[UIApplication sharedApplication] endBackgroundTask: self.backgroundUpdateTask]; self.backgroundUpdateTask = UIBackgroundTaskInvalid; }

Tengo una propiedad UIBackgroundTaskIdentifier para cada tarea de fondo

Código equivalente en Swift

func doUpdate () { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { let taskID = beginBackgroundUpdateTask() var response: NSURLResponse?, error: NSError?, request: NSURLRequest? let data = NSURLConnection.sendSynchronousRequest(request, returningResponse: &response, error: &error) // Do something with the result endBackgroundUpdateTask(taskID) }) } func beginBackgroundUpdateTask() -> UIBackgroundTaskIdentifier { return UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({}) } func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) { UIApplication.sharedApplication().endBackgroundTask(taskID) }