web-dev-qa-db-fra.com

Utilisation appropriée de beginBackgroundTaskWithExpirationHandler

Je ne sais pas trop quand et comment utiliser beginBackgroundTaskWithExpirationHandler.

Apple montre dans ses exemples l’utiliser dans applicationDidEnterBackground délégué pour avoir plus de temps pour effectuer une tâche importante, généralement une transaction réseau.

Lorsque je regarde mon application, il me semble que la plupart des éléments de mon réseau sont importants, et lorsque l'un d'entre eux est démarré, j'aimerais le compléter si l'utilisateur a appuyé sur le bouton d'accueil.

Donc, est-il accepté/une bonne pratique d’envelopper chaque transaction réseau (et je ne parle pas de télécharger une grande quantité de données, c’est plutôt du xml court) avec beginBackgroundTaskWithExpirationHandler pour être du côté sûr? 

94
Eyal

Si vous souhaitez que votre transaction réseau continue en arrière-plan, vous devez l'envelopper dans une tâche en arrière-plan. Il est également très important que vous appeliez endBackgroundTask lorsque vous avez terminé - sinon, l'application sera supprimée une fois le délai imparti écoulé.

Les miens ont quelque chose comme ça:

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

J'ai une propriété UIBackgroundTaskIdentifier pour chaque tâche en arrière-plan


Code équivalent dans 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)
}
153
Ashley Mills

La réponse acceptée est très utile et devrait aller dans la plupart des cas, mais deux choses me dérangent à ce sujet:

  1. Comme plusieurs personnes l'ont noté, le fait de stocker l'identificateur de tâche en tant que propriété signifie qu'il peut être écrasé si la méthode est appelée à plusieurs reprises, ce qui conduit à une tâche qui ne sera jamais terminée correctement jusqu'à ce que le système d'exploitation le force à expirer .

  2. Ce modèle nécessite une propriété unique pour chaque appel à beginBackgroundTaskWithExpirationHandler, ce qui semble fastidieux si vous avez une application plus grande avec de nombreuses méthodes réseau. 

Pour résoudre ces problèmes, j'ai écrit un singleton qui s'occupe de toute la plomberie et suit les tâches actives dans un dictionnaire. Aucune propriété nécessaire pour suivre les identificateurs de tâches. Semble bien travailler. L'utilisation est simplifiée à:

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

//do stuff

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

Si vous souhaitez éventuellement fournir un bloc d’achèvement allant au-delà de la tâche (intégrée), vous pouvez appeler: 

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

Code source pertinent disponible ci-dessous (les éléments singleton sont exclus pour des raisons de brièveté). Commentaires/réactions bienvenus. 

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

        }

    }
}
20
Joel

Voici une classe Swift qui encapsule l'exécution d'une tâche en arrière-plan:

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

Le moyen le plus simple de l'utiliser:

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

Si vous devez attendre un rappel de délégué avant de terminer, utilisez ce qui suit:

class MyClass {
    backgroundTask: BackgroundTask?

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

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

Comme indiqué ici et dans les réponses à d'autres SO questions, vous ne souhaitez PAS utiliser beginBackgroundTask uniquement au moment où votre application passera à l'arrière-plan; au contraire, vous devez utiliser une tâche en arrière-plan pour toute opération fastidieuse dont vous souhaitez assurer l'achèvement même si l'application ne passe en arrière-plan.

Par conséquent, votre code risque de se retrouver parsemé de répétitions du même code passe-partout pour appeler beginBackgroundTask et endBackgroundTask de manière cohérente. Pour éviter cette répétition, il est certainement raisonnable de vouloir regrouper le passe-partout dans une entité unique encapsulée.

J'aime certaines des réponses existantes pour le faire, mais je pense que le meilleur moyen consiste à utiliser une sous-classe Operation:

  • Vous pouvez mettre l'opération en file d'attente sur n'importe quelle OperationQueue et manipuler cette file comme bon vous semble. Par exemple, vous êtes libre d'annuler prématurément toute opération existante dans la file d'attente.

  • Si vous avez plusieurs tâches à effectuer, vous pouvez chaîner plusieurs opérations de tâche en arrière-plan. Dépendances du support des opérations.

  • La file d’opérations peut (et devrait) être une file d’arrière-plan; par conséquent, vous n'avez pas à vous soucier de l'exécution de code asynchrone dans votre tâche, car Operation est le code asynchrone. (En effet, il n’a aucun sens d’exécuter un autre niveau de code asynchrone dans une opération, car l’opération se terminerait avant que ce code ne puisse commencer. Si vous deviez le faire, vous utiliseriez une autre opération. )

Voici une sous-classe d'opération possible:

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?()
            UIApplication.shared.endBackgroundTask(bti) // cancellation
        }
        guard bti != .invalid else { return }
        whatToDo()
        UIApplication.shared.endBackgroundTask(bti) // completion
    }
}

Il devrait être évident de savoir comment utiliser cela, mais dans le cas contraire, imaginons que nous ayons une OperationQueue globale:

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

Donc, pour un lot de code prenant beaucoup de temps, nous dirions:

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

Si votre lot fastidieux de code peut être divisé en étapes, vous souhaiterez peut-être vous retirer plus tôt si votre tâche est annulée. Dans ce cas, revenez simplement prématurément après la fermeture. Notez que votre référence à la tâche depuis la fermeture doit être faible ou vous obtiendrez un cycle de conservation. Voici une illustration artificielle:

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)

Si vous devez effectuer un nettoyage si la tâche en arrière-plan est annulée prématurément, j'ai fourni une propriété facultative de gestionnaire cleanup (non utilisée dans les exemples précédents). Certaines autres réponses ont été critiquées pour ne pas inclure cela.

1
matt

J'ai implémenté la solution de Joel. Voici le code complet:

fichier .h:

#import <Foundation/Foundation.h>

@interface VMKBackgroundTaskManager : NSObject

+ (id) sharedTasks;

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

@end

fichier .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
vomako

Veuillez tout d'abord lire la documentation: https://developer.Apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio

La tâche en arrière-plan doit remplir les conditions suivantes:

  • La tâche en arrière-plan doit être signalée dès que possible, mais il n'est pas nécessaire que ce soit avant le début de notre tâche réelle. La méthode beginBackgroundTaskWithExpirationHandler: fonctionne de manière asynchrone. Par conséquent, si elle est appelée à la fin de applicationDidEnterBackground:, elle n'enregistrera pas la tâche en arrière-plan et appellera immédiatement le gestionnaire d'expiration.
  • Le gestionnaire d’expiration doit annuler notre tâche réelle et marquer la tâche d’arrière-plan comme étant terminée. Cela nous oblige à conserver l'identifiant de la tâche en arrière-plan stocké quelque part, par exemple en tant qu'attribut d'une classe. Cette propriété doit être sous notre contrôle pour qu’elle ne puisse pas être remplacée.
  • Le gestionnaire d'expiration est exécuté à partir du thread principal. Votre tâche réelle doit donc être thread-safe si vous souhaitez l'annuler.
  • Notre vraie tâche devrait être annulable. Cela signifie que notre tâche réelle devrait avoir la méthode cancel. Sinon, il y a un risque que la tâche soit terminée de manière imprévue, même si nous marquons la tâche en arrière-plan comme terminée.
  • Le code contenant beginBackgroundTaskWithExpirationHandler: peut être appelé partout et sur n’importe quel fil. Il ne doit pas nécessairement s'agir d'une méthode de délégué d'application applicationDidEnterBackground:.
  • Cela n’a aucun sens de le faire pour des opérations synchrones de moins de 5 secondes avec la méthode applicationDidEnterBackground: (veuillez lire doc https://developer.Apple.com/documentation/uikit/uiapplicationdelegate/1622997-applicationdidenterbackbackground?language=objc )
  • La méthode applicationDidEnterBackground doit être exécutée dans un délai inférieur à 5 secondes afin que toutes les tâches en arrière-plan soient lancées sur le deuxième thread.

Exemple:

class MySpecificBackgroundTask: NSObject, URLSessionDataDelegate {

    // MARK: - Properties

    let application: UIApplication
    var backgroundTaskIdentifier: UIBackgroundTaskIdentifier
    var task: URLSessionDataTask? = nil

    // MARK: - Initializers

    init(application: UIApplication) {
        self.application = application
        self.backgroundTaskIdentifier = UIBackgroundTaskInvalid
    }

    // MARK: - Actions

    func start() {
        self.backgroundTaskIdentifier = self.application.beginBackgroundTask {
            self.cancel()
        }

        self.startUrlRequest()
    }

    func cancel() {
        self.task?.cancel()
        self.end()
    }

    private func end() {
        self.application.endBackgroundTask(self.backgroundTaskIdentifier)
        self.backgroundTaskIdentifier = UIBackgroundTaskInvalid
    }

    // MARK: - URLSession methods

    private func startUrlRequest() {
        let sessionConfig = URLSessionConfiguration.background(withIdentifier: "MySpecificBackgroundTaskId")
        let session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
        guard let url = URL(string: "https://example.com/api/my/path") else {
            self.end()
            return
        }
        let request = URLRequest(url: url)
        self.task = session.dataTask(with: request)
        self.task?.resume()
    }

    // MARK: - URLSessionDataDelegate methods

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        self.end()
    }

    // Implement other methods of URLSessionDataDelegate to handle response...
}

Il peut être utilisé dans notre délégué d'application:

func applicationDidEnterBackground(_ application: UIApplication) {
    let myBackgroundTask = MySpecificBackgroundTask(application: application)
    myBackgroundTask.start()
}
0
Ariel Bogdziewicz