Recevez une notification lorsque NSOperationQueue termine toutes les tâches

92

NSOperationQueuea waitUntilAllOperationsAreFinished, mais je ne veux pas l'attendre de manière synchrone. Je veux simplement masquer l'indicateur de progression dans l'interface utilisateur lorsque la file d'attente se termine.

Quelle est la meilleure façon d'y parvenir?

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

Kornel
la source
Cochez ceci si vous utilisez GCD dans swift 3. stackoverflow.com/a/44562935/1522584
Abhijith

Réponses:

166

Utilisez KVO pour observer la operationspropriété de votre file d'attente, puis vous pouvez savoir si votre file d'attente est terminée en vérifiant [queue.operations count] == 0.

Quelque part dans le fichier dans lequel vous effectuez le KVO, déclarez un contexte pour le KVO comme celui-ci ( plus d'informations ):

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 vous NSOperationQueueêtes dans une propriété nommée queue)

À un moment donné, avant que votre objet ne se désarchive complètement (ou lorsqu'il cesse de se soucier de l'état de la file d'attente), vous devrez vous désinscrire de KVO comme ceci:

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


Addendum: iOS 4.0 a une NSOperationQueue.operationCountpropriété qui, selon la documentation, est conforme au KVO. Cette réponse fonctionnera toujours dans iOS 4.0, elle est donc toujours utile pour la compatibilité ascendante.

Nick Forge
la source
26
Je dirais que vous devriez utiliser l'accesseur de propriété, car il fournit une encapsulation à l'épreuve du temps (si vous décidez, par exemple, d'initialiser paresseusement la file d'attente). L'accès direct à une propriété par son ivar peut être considéré comme une optimisation prématurée, mais cela dépend vraiment du contexte exact. Le temps gagné en accédant directement à une propriété via son ivar sera généralement négligeable, à moins que vous ne fassiez référence à cette propriété plus de 100 à 1000 fois par seconde (comme une estimation incroyablement grossière).
Nick Forge
2
Tenté de voter contre en raison d'une mauvaise utilisation du KVO. Utilisation correcte décrite ici: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Nikolai Ruhe
19
@NikolaiRuhe Vous avez raison - utiliser ce code lors du sous-classement d'une classe qui utilise elle-même KVO pour observer operationCountsur le même NSOperationQueueobjet entraînerait potentiellement des bogues, auquel cas vous auriez besoin d'utiliser correctement l'argument de contexte. Il est peu probable que cela se produise, mais certainement possible. (Il est plus utile de préciser le problème réel que d'ajouter snark + un lien)
Nick Forge
6
J'ai trouvé une idée intéressante ici . J'ai utilisé cela pour sous-classer NSOperationQueue, ajouté une propriété NSOperation, 'finalOpearation', qui est définie comme dépendante de chaque opération ajoutée à la file d'attente. De toute évidence, il fallait remplacer addOperation: pour le faire. Ajout d'un protocole qui envoie un msg à un délégué lorsque finalOperation se termine. A travaillé jusqu'à présent.
pnizzle
1
Bien mieux! Je serai très heureux lorsque les options seront spécifiées et que l'appel removeObserver: est enveloppé par un @ try / @ catch - Ce n'est pas idéal mais la documentation Apple précise qu'il n'y a pas de sécurité lors de l'appel de removeObserver: ... if l'objet n'a pas d'enregistrement d'observateur, l'application plantera.
Austin
20

Si vous attendez (ou souhaitez) 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 être conscient que si un certain nombre d'opérations "courtes" sont ajoutées à une file d'attente, vous pouvez voir ce comportement à la place (car les opérations sont démarrées dans le cadre de leur 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 avaient été ajoutées à une série NSOperationQueue (c'est-à-dire maxConcurrentOperationCount = 1) et seulement quand elles étaient toutes terminées.

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

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

Chris Kane Cocoa Frameworks, Apple

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

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

:)

logiciel évolué
la source
Salut, savez-vous si et comment il est possible d'être averti lorsque chaque opération dans la file d'attente se termine en utilisant un NSOperationQueue avec maxConcurrentOperationCount = 1?
Sefran2
@fran: Je demanderais aux opérations d'afficher une notification à la fin. De cette façon, d'autres modules peuvent s'enregistrer en tant qu'observateurs et répondre à la fin de chacun. Si votre @selector prend un objet de notification, vous pouvez facilement récupérer l'objet qui a publié la notification, au cas où vous auriez besoin de plus de détails sur l'opération qui vient de se terminer.
logiciel évolué le
17

Que diriez-vous d'ajouter une NSOperation qui dépend de toutes les autres pour qu'elle s'exécute en dernier?

Surtout Oui
la source
1
Cela peut fonctionner, mais c'est une solution lourde, et il serait difficile à gérer si vous devez ajouter de nouvelles tâches à la file d'attente.
Kornel
c'est en fait très élégant et celui que j'ai préféré le plus! vous mon vote.
Yariv Nissim
1
Personnellement, c'est ma solution préférée. Vous pouvez facilement créer une NSBlockOperation simple pour le bloc d'achèvement qui dépend de toutes les autres opérations.
Puneet Sethi
Vous pouvez rencontrer un problème selon lequel NSBlockOperation n'est pas appelé lorsque la file d'attente est annulée. Vous devez donc effectuer votre propre opération qui crée une erreur lors de l'annulation et appelle un bloc avec un paramètre d'erreur.
malhal
C'est la meilleure réponse!
trappeur
12

Une alternative consiste à utiliser GCD. Référez-vous à ceci 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
});
nhisyam
la source
5

Voilà comment je fais.

Configurez la file d'attente et enregistrez-vous pour les modifications dans la propriété operations:

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

... et l'observateur (dans ce cas self) met en œuvre:

- (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" UIActivityIndicatorViewmontre que quelque chose se passe. Évidemment, vous pouvez changer pour convenir ...

Kris Jenkins
la source
2
Cette forboucle semble potentiellement coûteuse (et si vous annulez toutes les opérations en même temps? Cela n'obtiendrait-il pas des performances quadratiques lorsque la file d'attente est en cours de nettoyage?)
Kornel
Bien, mais soyez prudent avec les threads, car, selon la documentation: "... les notifications KVO associées à une file d'attente d'opérations peuvent apparaître dans n'importe quel thread." Vous devrez probablement déplacer le flux d'exécution vers la file d'attente des opérations principale avant de mettre à jour le spinner
Igor Vasilev
3

J'utilise une catégorie pour ce faire.

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

Utilisation :

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

brandonscript
la source
Pourquoi est-ce un achèvement ? Un NSOperationQueue ne se termine pas - il est simplement vide. L'état vide peut être entré plusieurs fois pendant la durée de vie d'un NSOperationQueue.
CouchDeveloper
Cela ne fonctionne pas si op1 et op2 se terminent avant l'appel de setCompletion.
malhal
Excellente réponse, juste une mise en garde que le bloc d'achèvement est appelé lorsque la file d'attente est terminée avec le démarrage de toute l'opération. Démarrage des opérations! = Les opérations sont terminées.
Saqib Saud
Hmm vieille réponse, mais je parierais qu'elle waitUntilFinisheddevrait l'êtreYES
brandonscript
3

À partir d' iOS 13.0 , les propriétés operationCount et operation sont obsolètes. Il est tout aussi simple de suivre vous-même le nombre d'opérations dans votre file d'attente et de déclencher une notification lorsqu'elles sont toutes terminées. Cet exemple fonctionne également avec une sous-classification asynchrone d' Opération .

class MyOperationQueue: OperationQueue {
            
    public var numberOfOperations: Int = 0 {
        didSet {
            if numberOfOperations == 0 {
                print("All operations completed.")
                
                NotificationCenter.default.post(name: .init("OperationsCompleted"), object: nil)
            }
        }
    }
    
    public var isEmpty: Bool {
        return numberOfOperations == 0
    }
    
    override func addOperation(_ op: Operation) {
        super.addOperation(op)
        
        numberOfOperations += 1
    }
    
    override func addOperations(_ ops: [Operation], waitUntilFinished wait: Bool) {
        super.addOperations(ops, waitUntilFinished: wait)
        
        numberOfOperations += ops.count
    }
    
    public func decrementOperationCount() {
        numberOfOperations -= 1
    }
}

Vous trouverez ci-dessous une sous-classe d'opération pour des opérations asynchrones faciles

class AsyncOperation: Operation {
    
    let queue: MyOperationQueue

enum State: String {
    case Ready, Executing, Finished
    
    fileprivate var keyPath: String {
        return "is" + rawValue
    }
}

var state = State.Ready {
    willSet {
        willChangeValue(forKey: newValue.keyPath)
        willChangeValue(forKey: state.keyPath)
    }
    
    didSet {
        didChangeValue(forKey: oldValue.keyPath)
        didChangeValue(forKey: state.keyPath)
        
        if state == .Finished {
            queue.decrementOperationCount()
        }
    }
}

override var isReady: Bool {
    return super.isReady && state == .Ready
}

override var isExecuting: Bool {
    return state == .Executing
}

override var isFinished: Bool {
    return state == .Finished
}

override var isAsynchronous: Bool {
    return true
}

public init(queue: MyOperationQueue) {
    self.queue = queue
    super.init()
}

override func start() {
    if isCancelled {
        state = .Finished
        return
    }
    
    main()
    state = .Executing
}

override func cancel() {
    state = .Finished
}

override func main() {
    fatalError("Subclasses must override main without calling super.")
}

}

Caleb Lindsey
la source
où la decrementOperationCount()méthode est-elle invoquée?
iksnae
@iksnae - J'ai mis à jour ma réponse avec une sous-catégorie d' Opération . J'utilise decrementOperationCount () dans le didSet de ma variable d' état . J'espère que cela t'aides!
Caleb Lindsey le
2

Qu'en est-il de l'utilisation de KVO pour observer la operationCountpropriété de la file d'attente? Ensuite, vous en entendrez parler lorsque la file d'attente devenait vide, et aussi lorsqu'elle cessait d'être vide. Gérer l'indicateur de progression peut être aussi simple que de faire quelque chose comme:

[indicator setHidden:([queue operationCount]==0)]
Sixten Otto
la source
Cela a-t-il fonctionné pour vous? Dans mon application, la version NSOperationQueue3.1 se plaint de ne pas être conforme au KVO pour la clé operationCount.
zoul
Je n'ai pas vraiment essayé cette solution dans une application, non. Je ne peux pas dire si l'OP l'a fait. Mais la documentation indique clairement que cela devrait fonctionner. Je déposerais un rapport de bogue. developer.apple.com/iphone/library/documentation/Cocoa/…
Sixten Otto
Il n'y a pas de propriété operationCount sur NSOperationQueue dans le SDK iPhone (du moins pas à partir de 3.1.3). Vous devez avoir consulté la page de documentation de Max OS X ( developer.apple.com/Mac/library/documentation/Cocoa/Reference/… )
Nick Forge
1
Le temps guérit toutes les blessures ... et parfois les mauvaises réponses. À partir d'iOS 4, la operationCountpropriété est présente.
Sixten Otto
2

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

}
pvllnspk
la source
3
lorsque les tâches sont exécutées simultanément, c'est une mauvaise approche.
Marcin
2
Et lorsque la file d'attente est annulée, cette dernière opération n'est même pas lancée.
malhal
2

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!");
    }
}];
Stunner
la source
1

Pour info, vous pouvez y parvenir avec GCD dispatch_group dans 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")
}
Abhijith
la source
Quelle est la version iOS minimale pour l'utiliser?
Nitesh Borad
Il est disponible à partir de swift 3, iOS 8 ou supérieur.
Abhijith
0

Vous pouvez créer un nouveau NSThread, ou exécuter un sélecteur en arrière-plan et y attendre. Quand leNSOperationQueue fin, vous pouvez envoyer une notification de votre choix.

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"];
}
pgb
la source
Il semble un peu idiot de créer un fil juste pour le mettre en veille.
Kornel
Je suis d'accord. Pourtant, je ne pouvais pas trouver un autre moyen de contourner cela.
pgb
Comment vous assureriez-vous qu'un seul thread attend? J'ai pensé au drapeau, mais cela doit être protégé contre les conditions de course, et j'ai fini par utiliser trop de NSLock à mon goût.
Kornel
Je pense que vous pouvez envelopper NSOperationQueue dans un autre objet. Chaque fois que vous mettez en file d'attente une NSOperation, vous incrémentez un nombre et lancez un thread. Chaque fois qu'un thread se termine, vous décrémentez ce nombre de un. Je pensais à un scénario où vous pourriez tout mettre en file d'attente à l'avance, puis démarrer la file d'attente, de sorte que vous n'auriez besoin que d'un seul thread en attente.
pgb le
0

Si vous utilisez cette opération comme classe de base, vous pouvez passer whenEmpty {}block à OperationQueue :

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

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

queue.whenEmpty = {
    print("all operations finished")
}
user1244109
la source
1
La valeur du type 'OperationQueue' n'a aucun membre 'whenEmpty'
Dale
@Dale si vous cliquez sur le lien, cela vous mènera à une page github où tout est expliqué. Si je me souviens bien, la réponse a été écrite lorsque OperationQueue de Foundation s'appelait encore NSOperationQueue; il y avait donc peut-être moins d'ambiguïté.
user1244109
Mon mauvais ... J'ai fait la fausse conclusion que "OperationQueue" ci-dessus était "OperationQueue" de Swift 4.
Dale
0

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)
    }
}
kasyanov-ms
la source
0

Si vous êtes ici à la recherche d'une solution avec combine - j'ai fini par écouter mon propre objet d'état.

@Published var state: OperationState = .ready
var sub: Any?

sub = self.$state.sink(receiveValue: { (state) in
 print("state updated: \(state)")
})
afanaian
la source