Uso corretto di beginBackgroundTaskWithExpirationHandler


107

Sono un po 'confuso su come e quando usarlo beginBackgroundTaskWithExpirationHandler.

Apple mostra nei loro esempi di usarlo in applicationDidEnterBackgrounddelegato, per avere più tempo per completare alcune attività importanti, di solito una transazione di rete.

Quando guardo sulla mia app, sembra che la maggior parte delle mie cose di rete sia importante e quando ne viene avviata una, vorrei completarla se l'utente preme il pulsante Home.

Quindi è accettata / buona pratica avvolgere ogni transazione di rete (e non sto parlando di scaricare grandi blocchi di dati, principalmente un breve xml) beginBackgroundTaskWithExpirationHandlerper essere al sicuro?


Vedi anche qui
Honey

Risposte:


165

Se desideri che la transazione di rete continui in background, dovrai includerla in un'attività in background. È anche molto importante che chiami endBackgroundTaskquando hai finito, altrimenti l'app verrà terminata allo scadere del tempo assegnato.

I miei tendono ad assomigliare a questo:

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

Ho una UIBackgroundTaskIdentifierproprietà per ogni attività in background


Codice equivalente in 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)
}

1
Sì, lo faccio ... altrimenti si fermano quando l'app entra in background.
Ashley Mills

1
dobbiamo fare qualcosa in applicationDidEnterBackground?
cali il

1
Solo se si desidera utilizzarlo come punto per avviare l'operazione di rete. Se vuoi solo completare un'operazione esistente, come da domanda di @ Eyal, non devi fare nulla in applicationDidEnterBackground
Ashley Mills

2
Grazie per questo chiaro esempio! (Appena cambiato beingBackgroundUpdateTask per beginBackgroundUpdateTask.)
newenglander

30
Se chiami doUpdate più volte di seguito senza che il lavoro sia terminato, sovrascrivi self.backgroundUpdateTask in modo che le attività precedenti non possano essere terminate correttamente. È necessario memorizzare l'identificatore dell'attività ogni volta in modo da terminarla correttamente o utilizzare un contatore nei metodi di inizio / fine.
thejaz

23

La risposta accettata è molto utile e dovrebbe andare bene nella maggior parte dei casi, tuttavia due cose mi hanno infastidito:

  1. Come molte persone hanno notato, memorizzare l'identificatore dell'attività come una proprietà significa che può essere sovrascritto se il metodo viene chiamato più volte, portando a un'attività che non verrà mai terminata correttamente fino a quando non sarà forzata dal sistema operativo alla scadenza del tempo .

  2. Questo modello richiede una proprietà univoca per ogni chiamata a beginBackgroundTaskWithExpirationHandlercui sembra complicato se si dispone di un'app più grande con molti metodi di rete.

Per risolvere questi problemi, ho scritto un singleton che si occupa di tutte le tubature e tiene traccia delle attività attive in un dizionario. Nessuna proprietà necessaria per tenere traccia degli identificatori di attività. Sembra funzionare bene. L'utilizzo è semplificato per:

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

//do stuff

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

Facoltativamente, se vuoi fornire un blocco di completamento che fa qualcosa oltre a terminare l'attività (che è incorporata) puoi chiamare:

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

Codice sorgente pertinente disponibile di seguito (materiale singleton escluso per brevità). Commenti / feedback benvenuti.

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

        }

    }
}

1
piace molto questa soluzione. una domanda però: come / come cosa hai fatto typedefCompletionBlock? Semplicemente questo:typedef void (^CompletionBlock)();
Joseph

Avete capito bene. typedef void (^ CompletionBlock) (void);
Joel

@joel, grazie ma dov'è il link del codice sorgente per questa implementazione, i, e, BackGroundTaskManager?
Özgür

Come notato sopra "roba singleton esclusa per brevità". [BackgroundTaskManager sharedTasks] restituisce un singleton. Le budella del singleton sono fornite sopra.
Joel

Votato per l'utilizzo di un singleton. Non credo proprio che siano così cattivi come la gente pensa!
Craig Watkinson

20

Ecco una classe Swift che incapsula l'esecuzione di un'attività in background:

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

Il modo più semplice per usarlo:

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

Se devi attendere la richiamata di un delegato prima di terminare, usa qualcosa di simile:

class MyClass {
    backgroundTask: BackgroundTask?

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

    func callback() {
        backgroundTask?.end()
        backgroundTask = nil
    } 
}

Lo stesso problema come nella risposta accettata. Il gestore della scadenza non annulla l'attività reale, ma la contrassegna solo come terminata. Inoltre, l'incapsulamento fa sì che non siamo in grado di farlo da soli. Ecco perché Apple ha esposto questo gestore, quindi l'incapsulamento è sbagliato qui.
Ariel Bogdziewicz

@ArielBogdziewicz È vero che questa risposta non offre alcuna possibilità di ulteriore pulizia nel beginmetodo, ma è facile vedere come aggiungere quella funzionalità.
matt

6

Come indicato qui e nelle risposte ad altre domande SO, NON vuoi usare beginBackgroundTasksolo quando la tua app andrà in background; al contrario, è necessario utilizzare un compito sfondo per qualsiasi operazione in termini di tempo di cui si desidera assicurare, anche se l'applicazione di completamento non va in secondo piano.

Pertanto è probabile che il tuo codice finisca per essere costellato di ripetizioni dello stesso codice standard per chiamare beginBackgroundTaske in modo endBackgroundTaskcoerente. Per evitare questa ripetizione, è certamente ragionevole voler impacchettare il boilerplate in una singola entità incapsulata.

Mi piacciono alcune delle risposte esistenti per farlo, ma penso che il modo migliore sia usare una sottoclasse Operazione:

  • Puoi accodare l'operazione su qualsiasi OperationQueue e manipolare quella coda come meglio credi. Ad esempio, sei libero di annullare anticipatamente qualsiasi operazione esistente sulla coda.

  • Se hai più di una cosa da fare, puoi concatenare più operazioni in background. Dipendenze del supporto operativo.

  • La coda delle operazioni può (e dovrebbe) essere una coda in background; quindi, non è necessario preoccuparsi di eseguire codice asincrono all'interno dell'attività, poiché l'operazione è il codice asincrono. (In effetti, non ha senso eseguire un altro livello di codice asincrono all'interno di un'operazione, poiché l'operazione finirebbe prima che quel codice possa iniziare. Se fosse necessario, useresti un'altra operazione.)

Ecco una possibile sottoclasse di operazioni:

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

Dovrebbe essere ovvio come usarlo, ma nel caso in cui non lo sia, immagina di avere una OperationQueue globale:

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

Quindi, per un tipico batch di codice che richiede tempo, diremmo:

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

Se il tuo batch di codice che richiede tempo può essere suddiviso in fasi, potresti voler abbandonare presto se l'attività viene annullata. In tal caso è sufficiente rientrare prematuramente dalla chiusura. Nota che il tuo riferimento all'attività dall'interno della chiusura deve essere debole o otterrai un ciclo di conservazione. Ecco un'illustrazione artificiale:

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)

Nel caso in cui sia necessario eseguire la pulizia nel caso in cui l'attività in background stessa venga annullata prematuramente, ho fornito una cleanupproprietà del gestore opzionale (non utilizzata negli esempi precedenti). Alcune altre risposte sono state criticate per non averlo incluso.


Ora ho fornito questo come un progetto github: github.com/mattneub/BackgroundTaskOperation
matt

1

Ho implementato la soluzione di Joel. Ecco il codice completo:

file .h:

#import <Foundation/Foundation.h>

@interface VMKBackgroundTaskManager : NSObject

+ (id) sharedTasks;

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

@end

File .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

1
Grazie per questo. Il mio obiettivo-c non è eccezionale. Potresti aggiungere del codice che mostri come usarlo?
pomo

puoi per favore fornire un esempio completo su come usare il tuo codice
Amr Angry

Molto bella. Grazie.
Alyoshak
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.