web-dev-qa-db-fra.com

Recevoir une notification lorsque NSOperationQueue termine toutes les tâches

NSOperationQueue a waitUntilAllOperationsAreFinished, mais je ne veux pas l'attendre de manière synchrone. Je veux juste cacher l'indicateur de progression dans l'interface utilisateur lorsque la file d'attente est terminée.

Quel est le meilleur moyen d'y parvenir?

Je ne peux pas envoyer de notifications de ma NSOperations, car je ne sais pas laquelle sera la dernière, et [queue operations] n'est peut-être pas encore vide (ou pire - repeuplé) lorsque la notification est reçue.

85
Kornel

Utilisez KVO pour observer la propriété operations de votre file d'attente. Vous pourrez ainsi savoir si votre file d'attente est terminée en recherchant [queue.operations count] == 0.

Quelque part dans le fichier dans lequel vous faites le KVO, déclarez un contexte pour le KVO comme ceci ( plus d'infos ):

static NSString *kQueueOperationsChanged = @"kQueueOperationsChanged";

Lorsque vous configurez votre file d'attente, procédez comme suit:

[self.queue addObserver:self forKeyPath:@"operations" options:0 context:&kQueueOperationsChanged];

Ensuite, faites ceci dans votre observeValueForKeyPath:

- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
                         change:(NSDictionary *)change context:(void *)context
{
    if (object == self.queue && [keyPath isEqualToString:@"operations"] && context == &kQueueOperationsChanged) {
        if ([self.queue.operations count] == 0) {
            // Do something here when your queue has completed
            NSLog(@"queue has completed");
        }
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object 
                               change:change context:context];
    }
}

(Cela suppose que votre NSOperationQueue se trouve dans une propriété nommée queue)

À un certain moment avant que votre objet ne traite complètement de ses deallocs (ou lorsqu'il ne se soucie plus de l'état de la file d'attente), vous devrez vous désinscrire de KVO comme suit:

[self.queue removeObserver:self forKeyPath:@"operations" context:&kQueueOperationsChanged];


Addendum: iOS 4.0 possède une propriété NSOperationQueue.operationCount qui, selon la documentation, est conforme à KVO. Cette réponse fonctionnera toujours sous iOS 4.0, ce qui la rend utile pour la compatibilité avec les versions antérieures.

161
Nick Forge

Si vous attendez (ou désirez) quelque chose qui correspond à ce comportement:

t=0 add an operation to the queue.  queueucount increments to 1
t=1 add an operation to the queue.  queueucount increments to 2
t=2 add an operation to the queue.  queueucount increments to 3
t=3 operation completes, queuecount decrements to 2
t=4 operation completes, queuecount decrements to 1
t=5 operation completes, queuecount decrements to 0
<your program gets notified that all operations are completed>

Vous devez savoir que si un certain nombre d'opérations "courtes" sont ajoutées à une file d'attente, vous risquez plutôt de rencontrer ce problème (car les opérations sont démarrées lors de l'ajout à la file d'attente):

t=0  add an operation to the queue.  queuecount == 1
t=1  operation completes, queuecount decrements to 0
<your program gets notified that all operations are completed>
t=2  add an operation to the queue.  queuecount == 1
t=3  operation completes, queuecount decrements to 0
<your program gets notified that all operations are completed>
t=4  add an operation to the queue.  queuecount == 1
t=5  operation completes, queuecount decrements to 0
<your program gets notified that all operations are completed>

Dans mon projet, j'avais besoin de savoir quand la dernière opération était terminée, après qu'un grand nombre d'opérations avait été ajouté à une série NSOperationQueue (c'est-à-dire, maxConcurrentOperationCount = 1) et seulement quand elles étaient toutes terminées.

J'ai trouvé cette déclaration d'un développeur Apple sur Google en réponse à la question "est-ce qu'une FIFO série NSoperationQueue?" - 

Si toutes les opérations ont la même priorité (qui n'est pas modifiée après L'opération est ajoutée à une file d'attente) et que toutes les opérations sont toujours - isReady == OUI au moment où ils sont mis dans la file d'attente des opérations, puis une série NSOperationQueue est FIFO. 

Chris Kane Cadres Cacao, Pomme

Dans mon cas, il est possible de savoir quand la dernière opération a été ajoutée à la file d'attente. Ainsi, une fois la dernière opération ajoutée, j'ajoute une autre opération à la file d'attente, de priorité inférieure, qui ne fait qu'envoyer la notification de vidage de la file d'attente. Compte tenu de la déclaration d’Apple, cela garantit que seul un avis est envoyé une fois toutes les opérations terminées.

Si les opérations sont ajoutées de manière à ne pas détecter le dernier (c'est-à-dire, non déterministe), alors je pense que vous devez suivre les approches KVO mentionnées ci-dessus, avec une logique de garde supplémentaire ajoutée pour tenter de détecter si les opérations peuvent être ajoutées.

:)

20
software evolved

Pourquoi ne pas ajouter un NSOperation dépendant de tous les autres afin qu’il soit exécuté en dernier?

16
MostlyYes

Une alternative consiste à utiliser GCD. Reportez-vous à this comme référence. 

dispatch_queue_t queue = dispatch_get_global_queue(0,0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group,queue,^{
 NSLog(@"Block 1");
 //run first NSOperation here
});

dispatch_group_async(group,queue,^{
 NSLog(@"Block 2");
 //run second NSOperation here
});

//or from for loop
for (NSOperation *operation in operations)
{
   dispatch_group_async(group,queue,^{
      [operation start];
   });
}

dispatch_group_notify(group,queue,^{
 NSLog(@"Final block");
 //hide progress indicator here
});
12
nhisyam

C'est comme ça que je le fais.

Configurez la file d'attente et enregistrez les modifications dans la propriété des opérations:

myQueue = [[NSOperationQueue alloc] init];
[myQueue addObserver: self forKeyPath: @"operations" options: NSKeyValueObservingOptionNew context: NULL];

... et l'observateur (dans ce cas self) implémente:

- (void) observeValueForKeyPath:(NSString *) keyPath ofObject:(id) object change:(NSDictionary *) change context:(void *) context {

    if (
        object == myQueue
        &&
        [@"operations" isEqual: keyPath]
    ) {

        NSArray *operations = [change objectForKey:NSKeyValueChangeNewKey];

        if ( [self hasActiveOperations: operations] ) {
            [spinner startAnimating];
        } else {
            [spinner stopAnimating];
        }
    }
}

- (BOOL) hasActiveOperations:(NSArray *) operations {
    for ( id operation in operations ) {
        if ( [operation isExecuting] && ! [operation isCancelled] ) {
            return YES;
        }
    }

    return NO;
}

Dans cet exemple, "spinner" est une UIActivityIndicatorView montrant qu'il se passe quelque chose. Évidemment, vous pouvez changer en fonction de ...

5
Kris Jenkins

Ajouter la dernière opération comme:

NSInvocationOperation *callbackOperation = [[NSInvocationOperation alloc] initWithTarget:object selector:selector object:nil];

Alors:

- (void)method:(id)object withSelector:(SEL)selector{
     NSInvocationOperation *callbackOperation = [[NSInvocationOperation alloc] initWithTarget:object selector:selector object:nil];
     [callbackOperation addDependency: ...];
     [operationQueue addOperation:callbackOperation]; 

}
2
pvllnspk

Pourquoi ne pas utiliser KVO pour observer la propriété operationCount de la file d'attente? Ensuite, vous en entendriez parler lorsque la file d'attente serait vidée et également lorsqu'elle cesserait d'être vide. Traiter avec l'indicateur de progression peut être aussi simple que de simplement faire quelque chose comme:

[indicator setHidden:([queue operationCount]==0)]
2
Sixten Otto

Avec ReactiveObjC Je trouve que cela fonctionne bien:

// skip 1 time here to ignore the very first call which occurs upon initialization of the RAC block
[[RACObserve(self.operationQueue, operationCount) skip:1] subscribeNext:^(NSNumber *operationCount) {
    if ([operationCount integerValue] == 0) {
         // operations are done processing
         NSLog(@"Finished!");
    }
}];
2
Stunner

J'utilise une catégorie pour faire cela.

NSOperationQueue + Completion.h

//
//  NSOperationQueue+Completion.h
//  QueueTest
//
//  Created by Artem Stepanenko on 23.11.13.
//  Copyright (c) 2013 Artem Stepanenko. All rights reserved.
//

typedef void (^NSOperationQueueCompletion) (void);

@interface NSOperationQueue (Completion)

/**
 * Remarks:
 *
 * 1. Invokes completion handler just a single time when previously added operations are finished.
 * 2. Completion handler is called in a main thread.
 */

- (void)setCompletion:(NSOperationQueueCompletion)completion;

@end

NSOperationQueue + Completion.m

//
//  NSOperationQueue+Completion.m
//  QueueTest
//
//  Created by Artem Stepanenko on 23.11.13.
//  Copyright (c) 2013 Artem Stepanenko. All rights reserved.
//

#import "NSOperationQueue+Completion.h"

@implementation NSOperationQueue (Completion)

- (void)setCompletion:(NSOperationQueueCompletion)completion
{
    NSOperationQueueCompletion copiedCompletion = [completion copy];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self waitUntilAllOperationsAreFinished];

        dispatch_async(dispatch_get_main_queue(), ^{
            copiedCompletion();
        });
    });
}

@end

Usage:

NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    // ...
}];

NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    // ...
}];

[operation2 addDependency:operation1];

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation1, operation2] waitUntilFinished:YES];

[queue setCompletion:^{
    // handle operation queue's completion here (launched in main thread!)
}];

Source: https://Gist.github.com/artemstepanenko/7620471

1
brandonscript

Pour info, vous pouvez y parvenir avec GCD dispatch_group in Swift 3 . Vous pouvez être averti lorsque toutes les tâches sont terminées.

let group = DispatchGroup()

    group.enter()
    run(after: 6) {
      print(" 6 seconds")
      group.leave()
    }

    group.enter()
    run(after: 4) {
      print(" 4 seconds")
      group.leave()
    }

    group.enter()
    run(after: 2) {
      print(" 2 seconds")
      group.leave()
    }

    group.enter()
    run(after: 1) {
      print(" 1 second")
      group.leave()
    }


    group.notify(queue: DispatchQueue.global(qos: .background)) {
      print("All async calls completed")
}
1

Si vous utilisez cette opération Operation comme classe de base, vous pouvez passer le bloc whenEmpty {} au OperationQueue :

let queue = OOperationQueue()
queue.addOperation(op)
queue.addOperation(delayOp)

queue.addExecution { finished in
    delay(0.5) { finished() }
}

queue.whenEmpty = {
    print("all operations finished")
}
0
user1244109

Sans KVO

private let queue = OperationQueue()

private func addOperations(_ operations: [Operation], completionHandler: @escaping () -> ()) {
    DispatchQueue.global().async { [unowned self] in
        self.queue.addOperations(operations, waitUntilFinished: true)
        DispatchQueue.main.async(execute: completionHandler)
    }
}
0
kasyanov-ms

Vous pouvez créer une nouvelle NSThread, ou exécuter un sélecteur en arrière-plan et attendre. Lorsque la NSOperationQueue est terminée, vous pouvez envoyer votre propre notification.

Je pense à quelque chose comme:

- (void)someMethod {
    // Queue everything in your operationQueue (instance variable)
    [self performSelectorInBackground:@selector(waitForQueue)];
    // Continue as usual
}

...

- (void)waitForQueue {
    [operationQueue waitUntilAllOperationsAreFinished];
    [[NSNotificationCenter defaultCenter] postNotification:@"queueFinished"];
}
0
pgb