Uso adecuado de beginBackgroundTaskWithExpirationHandler

107

Estoy un poco confundido acerca de cómo y cuándo usarlo beginBackgroundTaskWithExpirationHandler.

Apple muestra en sus ejemplos usarlo en applicationDidEnterBackgrounddelegado, para tener más tiempo para completar alguna tarea importante, generalmente una transacción de red.

Al mirar mi aplicación, parece que la mayoría de las cosas de mi red son importantes, y cuando se inicia una, me gustaría completarla si el usuario presionó el botón de inicio.

Entonces, ¿es aceptado / una buena práctica envolver cada transacción de red (y no estoy hablando de descargar una gran cantidad de datos, en su mayoría un XML corto) beginBackgroundTaskWithExpirationHandlerpara estar seguro?

Eyal
fuente
Ver también aquí
Cariño

Respuestas:

165

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 endBackgroundTaskcuando haya terminado; de lo contrario, la aplicación se eliminará una vez que haya expirado el tiempo asignado.

Los míos tienden a verse así:

- (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 UIBackgroundTaskIdentifierpropiedad para cada tarea en segundo plano.


Código equivalente en Swift

func doUpdate () {

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {

        let taskID = beginBackgroundUpdateTask()

        var response: URLResponse?, 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.shared.beginBackgroundTask(expirationHandler: ({}))
}

func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) {
    UIApplication.shared.endBackgroundTask(taskID)
}
Ashley Mills
fuente
1
Sí, lo hago ... de lo contrario, se detienen cuando la aplicación entra en segundo plano.
Ashley Mills
1
¿Necesitamos hacer algo en applicationDidEnterBackground?
cae el
1
Solo si desea usar eso como un punto para iniciar la operación de red. Si solo desea que se complete una operación existente, según la pregunta de @ Eyal, no necesita hacer nada en applicationDidEnterBackground
Ashley Mills
2
¡Gracias por este claro ejemplo! (Acabo de cambiar beingBackgroundUpdateTask a beginBackgroundUpdateTask.)
newenglander
30
Si llama a doUpdate varias veces seguidas sin que el trabajo esté terminado, sobrescribirá self.backgroundUpdateTask para que las tareas anteriores no se puedan finalizar correctamente. Debe almacenar el identificador de la tarea cada vez para finalizarla correctamente o utilizar un contador en los métodos de inicio / fin.
thejaz
23

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

  1. Como han señalado varias personas, almacenar el identificador de la tarea como una propiedad significa que se puede sobrescribir si el método se llama varias veces, lo que lleva a una tarea que nunca terminará correctamente hasta que el sistema operativo la obligue a finalizar en el momento de vencimiento. .

  2. Este patrón requiere una propiedad única para cada llamada, lo beginBackgroundTaskWithExpirationHandlerque 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 funcionar bien. El uso se simplifica a:

//start the task
NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTask];

//do stuff

//end the task
[[BackgroundTaskManager sharedTasks] endTaskWithKey:taskKey];

De manera opcional, si desea proporcionar un bloque de finalización que haga algo más que finalizar la tarea (que está integrada), puede llamar a:

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

El código fuente relevante está disponible a continuación (el material singleton se excluye por brevedad). Comentarios / retroalimentación 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]];

        }

    }
}
Joel
fuente
1
Me gusta mucho esta solución. Sin embargo, una pregunta: ¿cómo / cómo hiciste typedefCompletionBlock? Simplemente esto:typedef void (^CompletionBlock)();
Joseph
Lo tienes. typedef void (^ CompletionBlock) (vacío);
Joel
@joel, gracias pero ¿dónde está el enlace del código fuente para esta implementación, i, e, BackGroundTaskManager?
Özgür
Como se señaló anteriormente, "las cosas singleton excluidas por brevedad". [BackgroundTaskManager sharedTasks] devuelve un singleton. Las entrañas del singleton se proporcionan arriba.
Joel
Votó a favor por usar un singleton. ¡Realmente no creo que sean tan malos como la gente cree!
Craig Watkinson
20

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 utilizarlo:

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

Si necesita esperar una devolución de llamada de delegado antes de finalizar, use 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
    } 
}
phatmann
fuente
El mismo problema que en la respuesta aceptada. El controlador de vencimiento no cancela la tarea real, solo la marca como finalizada. Más sobre la encapsulación hace que no podamos hacer eso nosotros mismos. Es por eso que Apple expuso este controlador, por lo que la encapsulación está mal aquí.
Ariel Bogdziewicz
@ArielBogdziewicz Es cierto que esta respuesta no brinda la oportunidad de realizar una limpieza adicional en el beginmétodo, pero es fácil ver cómo agregar esa característica.
Matt
6

Como se indica aquí y en las respuestas a otras preguntas de SO, NO desea usar beginBackgroundTasksolo cuando su aplicación pasará a segundo plano; por el contrario, se debe utilizar una tarea de fondo para cualquier operación que consume tiempo cuya finalización desea asegurarse incluso si la aplicación no entra en el fondo.

Por lo tanto, es probable que su código termine salpicado de repeticiones del mismo código repetitivo para llamar beginBackgroundTasky de forma endBackgroundTaskcoherente. Para evitar esta repetición, es ciertamente razonable querer empaquetar el texto estándar en una sola entidad encapsulada.

Me gustan algunas de las respuestas existentes para hacer eso, pero creo que la mejor manera es usar una subclase de Operación:

  • Puede poner la operación en cola en cualquier OperationQueue y manipular esa cola como mejor le parezca. Por ejemplo, puede cancelar prematuramente cualquier operación existente en la cola.

  • Si tiene más de una cosa que hacer, puede encadenar varias operaciones de tareas en segundo plano. Dependencias de soporte de operaciones.

  • Operation Queue puede (y debe) ser una cola en segundo plano; por lo tanto, no hay necesidad de preocuparse por realizar código asincrónico dentro de su tarea, porque la Operación es el código asincrónico. (De hecho, no tiene sentido ejecutar otro nivel de código asincrónico dentro de una Operación, ya que la Operación terminaría antes de que ese código pudiera siquiera comenzar. Si necesitara hacer eso, usaría otra Operación).

Aquí hay una posible subclase de Operación:

class BackgroundTaskOperation: Operation {
    var whatToDo : (() -> ())?
    var cleanup : (() -> ())?
    override func main() {
        guard !self.isCancelled else { return }
        guard let whatToDo = self.whatToDo else { return }
        var bti : UIBackgroundTaskIdentifier = .invalid
        bti = UIApplication.shared.beginBackgroundTask {
            self.cleanup?()
            self.cancel()
            UIApplication.shared.endBackgroundTask(bti) // cancellation
        }
        guard bti != .invalid else { return }
        whatToDo()
        guard !self.isCancelled else { return }
        UIApplication.shared.endBackgroundTask(bti) // completion
    }
}

Debería ser obvio cómo usar esto, pero en caso de que no lo sea, imagine que tenemos una OperationQueue global:

let backgroundTaskQueue : OperationQueue = {
    let q = OperationQueue()
    q.maxConcurrentOperationCount = 1
    return q
}()

Entonces, para un lote de código típico que consume mucho tiempo, diríamos:

let task = BackgroundTaskOperation()
task.whatToDo = {
    // do something here
}
backgroundTaskQueue.addOperation(task)

Si su lote de código que consume mucho tiempo se puede dividir en etapas, es posible que desee retirarse antes si se cancela su tarea. En ese caso, simplemente regrese prematuramente del cierre. Tenga en cuenta que su referencia a la tarea desde dentro del cierre debe ser débil o obtendrá un ciclo de retención. Aquí hay una ilustración artificial:

let task = BackgroundTaskOperation()
task.whatToDo = { [weak task] in
    guard let task = task else {return}
    for i in 1...10000 {
        guard !task.isCancelled else {return}
        for j in 1...150000 {
            let k = i*j
        }
    }
}
backgroundTaskQueue.addOperation(task)

En caso de que tenga que hacer una limpieza en caso de que la tarea en segundo plano se cancele prematuramente, proporcioné una cleanuppropiedad de controlador opcional (no se usa en los ejemplos anteriores). Algunas otras respuestas fueron criticadas por no incluir eso.

mate
fuente
Ahora proporcioné esto como un proyecto de github
matt
1

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

Archivo .h:

#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
vomako
fuente
1
Gracias por esto. Mi objetivo-c no es genial. ¿Podría agregar algún código que muestre cómo usarlo?
pomo
¿Puede dar un ejemplo completo sobre cómo usar su código
Amr Angry
Muy agradable. Gracias.
Alyoshak