Uso adequado de beginBackgroundTaskWithExpirationHandler

107

Estou um pouco confuso sobre como e quando usar beginBackgroundTaskWithExpirationHandler.

A Apple mostra em seus exemplos como usá-lo em applicationDidEnterBackgrounddelegação, para obter mais tempo para concluir alguma tarefa importante, geralmente uma transação de rede.

Ao olhar em meu aplicativo, parece que a maioria das coisas da minha rede é importante e, quando um for iniciado, gostaria de concluí-lo se o usuário pressionasse o botão home.

Portanto, é aceitável / boa prática agrupar todas as transações de rede (e não estou falando sobre o download de grandes blocos de dados, principalmente alguns xml curtos) beginBackgroundTaskWithExpirationHandlerpara ficar no lado seguro?

Eyal
fonte
Veja também aqui
Honey

Respostas:

165

Se você deseja que sua transação de rede continue em segundo plano, será necessário envolvê-la em uma tarefa em segundo plano. Também é muito importante que você ligue endBackgroundTaskquando terminar - caso contrário, o aplicativo será encerrado após o tempo alocado expirar.

Os meus tendem a ser mais ou menos assim:

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

Eu tenho uma UIBackgroundTaskIdentifierpropriedade para cada tarefa em segundo plano


Código equivalente em 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
fonte
1
Sim, eu quero ... caso contrário, eles param quando o aplicativo entra em segundo plano.
Ashley Mills
1
precisamos fazer alguma coisa em applicationDidEnterBackground?
mergulhos em
1
Somente se você quiser usar isso como um ponto para iniciar a operação da rede. Se você deseja apenas que uma operação existente seja concluída, conforme a pergunta de @Eyal, você não precisa fazer nada em applicationDidEnterBackground
Ashley Mills
2
Obrigado por este exemplo claro! (Acabei de mudar beingBackgroundUpdateTask para beginBackgroundUpdateTask.)
newenglander
30
Se você chamar doUpdate várias vezes consecutivas sem que o trabalho seja concluído, você substituirá self.backgroundUpdateTask para que as tarefas anteriores não possam ser encerradas corretamente. Você deve armazenar o identificador de tarefa a cada vez para finalizá-lo adequadamente ou usar um contador nos métodos de início / término.
thejaz 01 de
23

A resposta aceita é muito útil e deve ser adequada na maioria dos casos, no entanto, duas coisas me incomodaram:

  1. Como várias pessoas notaram, armazenar o identificador de tarefa como uma propriedade significa que ele pode ser sobrescrito se o método for chamado várias vezes, levando a uma tarefa que nunca será encerrada normalmente até que seja forçada pelo sistema operacional na expiração do tempo .

  2. Esse padrão requer uma propriedade exclusiva para cada chamada, o beginBackgroundTaskWithExpirationHandlerque parece complicado se você tiver um aplicativo maior com muitos métodos de rede.

Para resolver esses problemas, escrevi um singleton que cuida de todo o encanamento e rastreia as tarefas ativas em um dicionário. Nenhuma propriedade necessária para controlar os identificadores de tarefa. Parece funcionar bem. O uso é simplificado para:

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

//do stuff

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

Opcionalmente, se você deseja fornecer um bloco de conclusão que faz algo além de encerrar a tarefa (que está embutido), você pode chamar:

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

Código-fonte relevante disponível abaixo (itens singleton excluídos por questões de brevidade). Comentários / feedback bem-vindos.

- (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
fonte
1
realmente gosto desta solução. Mas uma pergunta: como / como você fez typedefCompletionBlock? Simplesmente isto:typedef void (^CompletionBlock)();
Joseph
Você entendeu. typedef void (^ CompletionBlock) (void);
Joel
@joel, obrigado, mas onde está o link do código-fonte para esta implementação, i, e, BackGroundTaskManager?
Özgür
Como observado acima, "material único excluído por brevidade". [BackgroundTaskManager sharedTasks] retorna um singleton. As entranhas do singleton são fornecidas acima.
Joel
Votado por usar um singleton. Eu realmente não acho que eles são tão ruins quanto as pessoas fazem!
Craig Watkinson
20

Aqui está uma classe Swift que encapsula a execução de uma tarefa em 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
    }
}

A maneira mais simples de usar:

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

Se você precisar esperar por um retorno de chamada de delegado antes de encerrar, use algo assim:

class MyClass {
    backgroundTask: BackgroundTask?

    func doSomething() {
        backgroundTask = BackgroundTask(application)
        backgroundTask!.begin()
        // Do something that waits for callback
    }

    func callback() {
        backgroundTask?.end()
        backgroundTask = nil
    } 
}
Phatmann
fonte
O mesmo problema como na resposta aceita. O gerenciador de expiração não cancela a tarefa real, apenas a marca como encerrada. Mais encapsulamento excessivo faz com que não possamos fazer isso sozinhos. É por isso que a Apple expôs esse manipulador, então o encapsulamento está errado aqui.
Ariel Bogdziewicz
@ArielBogdziewicz É verdade que essa resposta não oferece oportunidade para limpeza adicional no beginmétodo, mas é fácil ver como adicionar esse recurso.
matt de
6

Conforme observado aqui e em respostas a outras perguntas do SO, você NÃO deseja usar beginBackgroundTaskapenas quando seu aplicativo ficará em segundo plano; pelo contrário, você deve usar uma tarefa de fundo para qualquer operação demorada cuja conclusão você quiser garantir, mesmo que o aplicativo faz ir para o fundo.

Portanto, é provável que seu código acabe salpicado de repetições do mesmo código clichê para chamar beginBackgroundTaskeendBackgroundTask coerente. Para evitar essa repetição, é certamente razoável querer empacotar o clichê em uma única entidade encapsulada.

Gosto de algumas das respostas existentes para fazer isso, mas acho que a melhor maneira é usar uma subclasse de Operação:

  • Você pode enfileirar a operação em qualquer OperationQueue e manipular essa fila como achar melhor. Por exemplo, você está livre para cancelar prematuramente qualquer operação existente na fila.

  • Se você tem mais de uma coisa a fazer, pode encadear várias operações de tarefas em segundo plano. Dependências de suporte de operações.

  • A fila de operações pode (e deve) ser uma fila de segundo plano; assim, não há necessidade de se preocupar em realizar código assíncrono dentro de sua tarefa, pois a Operação é o código assíncrono. (Na verdade, não faz sentido executar outro nível de código assíncrono dentro de uma operação, pois a operação terminaria antes mesmo que o código pudesse começar. Se você precisasse fazer isso, usaria outra operação.)

Aqui está uma possível subclasse de Operação:

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

Deve ser óbvio como usar isso, mas caso não seja, imagine que temos uma OperationQueue global:

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

Portanto, para um lote típico de código demorado, diríamos:

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

Se seu lote de código demorado pode ser dividido em estágios, convém encerrar mais cedo se a tarefa for cancelada. Nesse caso, basta retornar prematuramente do fechamento. Observe que sua referência à tarefa de dentro do fechamento precisa ser fraca ou você obterá um ciclo de retenção. Aqui está uma ilustração 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)

Caso você tenha que fazer uma limpeza, caso a própria tarefa em segundo plano seja cancelada prematuramente, forneci uma cleanuppropriedade de manipulador opcional (não usada nos exemplos anteriores). Algumas outras respostas foram criticadas por não incluir isso.

mate
fonte
Agora forneci isso como um projeto github: github.com/mattneub/BackgroundTaskOperation
matt
1

Implementei a solução do Joel. Aqui está o código completo:

arquivo .h:

#import <Foundation/Foundation.h>

@interface VMKBackgroundTaskManager : NSObject

+ (id) sharedTasks;

- (NSUInteger)beginTask;
- (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion;
- (void)endTaskWithKey:(NSUInteger)_key;

@end

arquivo .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
fonte
1
Obrigado por isso. Meu objetivo-c não é ótimo. Você poderia adicionar algum código que mostre como usá-lo?
pomo
você pode dar um exemplo completo de como usar seu código
Amr Angry
Muito agradável. Obrigado.
Alyoshak de