Utilisation correcte de beginBackgroundTaskWithExpirationHandler

107

Je ne sais pas comment et quand l'utiliser beginBackgroundTaskWithExpirationHandler.

Apple montre dans leurs exemples de l'utiliser en applicationDidEnterBackgrounddélégué, pour avoir plus de temps pour effectuer une tâche importante, généralement une transaction réseau.

Lorsque vous regardez sur mon application, il semble que la plupart de mes informations réseau soient importantes, et quand l'une d'entre elles est lancée, j'aimerais la terminer si l'utilisateur appuie sur le bouton d'accueil.

Est-il donc accepté / bonne pratique d'encapsuler chaque transaction réseau (et je ne parle pas de télécharger un gros morceau de données, il s'agit principalement d'un fichier XML court) beginBackgroundTaskWithExpirationHandlerpour être du bon côté?

Eyal
la source
Voir aussi ici
Honey

Réponses:

165

Si vous souhaitez que votre transaction réseau se poursuive en arrière-plan, vous devrez l'envelopper dans une tâche en arrière-plan. Il est également très important que vous appeliez endBackgroundTasklorsque vous avez terminé, sinon l'application sera supprimée une fois le temps imparti écoulé.

Les miens ont tendance à ressembler à ceci:

- (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 UIBackgroundTaskIdentifierpropriété pour chaque tâche d'arrière-plan


Code équivalent 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
la source
1
Oui, je le fais ... sinon ils s'arrêtent lorsque l'application entre en arrière-plan.
Ashley Mills
1
devons-nous faire quelque chose dans applicationDidEnterBackground?
plonge
1
Uniquement si vous souhaitez l'utiliser comme point de départ de l'opération réseau. Si vous voulez juste qu'une opération existante se termine, selon la question de @ Eyal, vous n'avez rien à faire dans applicationDidEnterBackground
Ashley Mills
2
Merci pour cet exemple clair! (Vient de changer beingBackgroundUpdateTask en beginBackgroundUpdateTask.)
newenglander
30
Si vous appelez doUpdate plusieurs fois de suite sans que le travail soit terminé, vous écraserez self.backgroundUpdateTask afin que les tâches précédentes ne puissent pas être terminées correctement. Vous devez soit stocker l'identifiant de la tâche à chaque fois pour la terminer correctement, soit utiliser un compteur dans les méthodes begin / end.
thejaz
23

La réponse acceptée est très utile et devrait convenir dans la plupart des cas, mais deux choses m'ont dérangé:

  1. Comme un certain nombre de personnes l'ont noté, le stockage de l'identificateur de tâche en tant que propriété signifie qu'il peut être écrasé si la méthode est appelée plusieurs fois, ce qui conduit à une tâche qui ne sera jamais terminée correctement jusqu'à ce qu'elle soit forcée de se terminer par le système d'exploitation au moment de l'expiration. .

  2. Ce modèle nécessite une propriété unique pour chaque appel, beginBackgroundTaskWithExpirationHandlerce 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 fonctionner. L'utilisation est simplifiée pour:

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

//do stuff

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

Facultativement, si vous souhaitez fournir un bloc d'achèvement qui fait quelque chose au-delà de la fin de la tâche (qui est intégrée), vous pouvez appeler:

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

Code source pertinent disponible ci-dessous (trucs singleton exclus par souci de concision). Commentaires / commentaires 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]];

        }

    }
}
Joël
la source
1
aime vraiment cette solution. une question cependant: comment / comme qu'avez-vous typedefCompletionBlock? Simplement ceci:typedef void (^CompletionBlock)();
Joseph
Tu l'as eu. typedef void (^ CompletionBlock) (void);
Joel
@joel, merci mais où est le lien du code source pour cette implémentation, i, e, BackGroundTaskManager?
Özgür
Comme indiqué ci-dessus, "trucs singleton exclus par souci de concision". [BackgroundTaskManager sharedTasks] renvoie un singleton. Les tripes du singleton sont fournies ci-dessus.
Joel
J'ai voté pour l'utilisation d'un singleton. Je ne pense vraiment pas qu'ils soient aussi mauvais que les gens le prétendent!
Craig Watkinson
20

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

La façon la 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 quelque chose comme ceci:

class MyClass {
    backgroundTask: BackgroundTask?

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

    func callback() {
        backgroundTask?.end()
        backgroundTask = nil
    } 
}
Phatmann
la source
Le même problème comme dans la réponse acceptée. Le gestionnaire d'expiration n'annule pas la tâche réelle, mais la marque uniquement comme terminée. De plus, l'encapsulation fait que nous ne sommes pas capables de le faire nous-mêmes. C'est pourquoi Apple a exposé ce gestionnaire, l'encapsulation est donc erronée ici.
Ariel Bogdziewicz
@ArielBogdziewicz Il est vrai que cette réponse n'offre aucune possibilité de nettoyage supplémentaire dans la beginméthode, mais il est facile de voir comment ajouter cette fonctionnalité.
mat
6

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

Par conséquent, votre code est susceptible de se retrouver parsemé de répétitions du même code standard pour appeler beginBackgroundTasket de manière endBackgroundTaskcohérente. Pour éviter cette répétition, il est certainement raisonnable de vouloir emballer le passe-partout dans une seule entité encapsulée.

J'aime certaines des réponses existantes pour ce faire, mais je pense que le meilleur moyen est d'utiliser une sous-classe d'opération:

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

  • Si vous avez plus d'une chose à faire, vous pouvez enchaîner plusieurs opérations de tâche d'arrière-plan. Les opérations prennent en charge les dépendances.

  • La file d'attente des opérations peut (et devrait) être une file d'attente d'arrière-plan; ainsi, il n'est pas nécessaire de s'inquiéter de l'exécution de code asynchrone à l'intérieur de votre tâche, car l'opération est le code asynchrone. (En effet, cela 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 même démarrer. Si vous deviez faire cela, 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?()
            self.cancel()
            UIApplication.shared.endBackgroundTask(bti) // cancellation
        }
        guard bti != .invalid else { return }
        whatToDo()
        guard !self.isCancelled else { return }
        UIApplication.shared.endBackgroundTask(bti) // completion
    }
}

Il devrait être évident de savoir comment l'utiliser, mais au cas où ce ne serait pas le cas, imaginez que nous ayons une OperationQueue globale:

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

Donc, pour un lot de code qui prend du temps, nous dirions:

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

Si votre lot de code chronophage 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 prématurément de la fermeture. Notez que votre référence à la tâche à partir de la fermeture doit être faible ou vous obtiendrez un cycle de rétention. 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 avez un nettoyage à faire au cas où la tâche d'arrière-plan elle-même serait annulée prématurément, j'ai fourni une option cleanup propriété de gestionnaire (non utilisée dans les exemples précédents). Certaines autres réponses ont été critiquées pour ne pas avoir inclus cela.

mat
la source
Je l'ai maintenant fourni en tant que projet github: github.com/mattneub/BackgroundTaskOperation
matt
1

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
vomako
la source
1
Merci pour cela. Mon objectif-c n'est pas génial. Pourriez-vous ajouter du code qui montre comment l'utiliser?
pomo
pouvez-vous s'il vous plaît donner un exemple complet sur la façon d'utiliser votre code
Amr Angry
Très agréable. Merci.
Alyoshak