Ricevi una notifica quando NSOperationQueue termina tutte le attività


92

NSOperationQueueha waitUntilAllOperationsAreFinished, ma non voglio aspettare in modo sincrono. Voglio solo nascondere l'indicatore di avanzamento nell'interfaccia utente al termine della coda.

Qual è il modo migliore per farlo?

Non posso inviare notifiche dai miei NSOperation, perché non so quale sarà l'ultima e [queue operations]potrebbe non essere ancora vuota (o peggio - ripopolata) quando la notifica viene ricevuta.


Controlla se stai utilizzando GCD in swift 3. stackoverflow.com/a/44562935/1522584
Abhijith

Risposte:


166

Usa KVO per osservare la operationsproprietà della tua coda, quindi puoi sapere se la tua coda è stata completata controllando [queue.operations count] == 0.

Da qualche parte nel file in cui stai eseguendo il KVO, dichiara un contesto per KVO come questo ( maggiori informazioni ):

static NSString *kQueueOperationsChanged = @"kQueueOperationsChanged";

Quando imposti la coda, fai questo:

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

Quindi fallo nel tuo 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];
    }
}

(Questo presuppone che tu NSOperationQueuesia in una proprietà denominata queue)

Ad un certo punto prima che il tuo oggetto si deallochi completamente (o quando smette di preoccuparsi dello stato della coda), dovrai annullare la registrazione da KVO in questo modo:

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


Addendum: iOS 4.0 ha una NSOperationQueue.operationCountproprietà, che secondo i documenti è conforme a KVO. Questa risposta funzionerà comunque in iOS 4.0, quindi è ancora utile per la compatibilità con le versioni precedenti.


26
Direi che dovresti usare la funzione di accesso alle proprietà, poiché fornisce un incapsulamento a prova di futuro (se decidi ad esempio di inizializzare pigramente la coda). L'accesso diretto a una proprietà tramite il suo ivar potrebbe essere considerato un'ottimizzazione prematura, ma in realtà dipende dal contesto esatto. Il tempo risparmiato accedendo direttamente a una proprietà tramite il suo ivar di solito sarà trascurabile, a meno che non si faccia riferimento a quella proprietà più di 100-1000 volte al secondo (come stima incredibilmente grossolana).
Nick Forge

2
Tentato di downvote a causa del cattivo utilizzo di KVO. Utilizzo corretto descritto qui: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Nikolai Ruhe

19
@NikolaiRuhe Hai ragione: usare questo codice quando si crea una sottoclasse di una classe che a sua volta usa KVO per osservare operationCountsullo stesso NSOperationQueueoggetto potrebbe potenzialmente portare a bug, nel qual caso dovresti usare correttamente l'argomento context. È improbabile che accada, ma sicuramente possibile. (Spiegare il problema reale è più utile che aggiungere snark + un link)
Nick Forge

6
Ho trovato un'idea interessante qui . L'ho usato per sottoclassare NSOperationQueue, ho aggiunto una proprietà NSOperation, 'finalOpearation', che è impostata come dipendente da ciascuna operazione aggiunta alla coda. Ovviamente ha dovuto sovrascrivere addOperation: per farlo. Aggiunto anche un protocollo che invia un messaggio a un delegato al termine di finalOperation. Finora ha funzionato.
pnizzle

1
Molto meglio! Sarò molto felice quando le opzioni vengono specificate e la chiamata removeObserver: è racchiusa da un @ try / @ catch - Non è l'ideale ma i documenti Apple specificano che non c'è sicurezza quando si chiama removeObserver: ... if l'oggetto non ha una registrazione di osservatore l'applicazione andrà in crash.
Austin

20

Se ti aspetti (o desideri) qualcosa che corrisponda a questo comportamento:

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>

È necessario essere consapevoli del fatto che se una serie di operazioni "brevi" vengono aggiunte a una coda, è possibile invece vedere questo comportamento (perché le operazioni vengono avviate come parte dell'aggiunta alla coda):

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>

Nel mio progetto avevo bisogno di sapere quando l'ultima operazione è stata completata, dopo che un gran numero di operazioni era stato aggiunto a un NSOperationQueue seriale (cioè, maxConcurrentOperationCount = 1) e solo quando erano state tutte completate.

Cercando su Google ho trovato questa affermazione di uno sviluppatore Apple in risposta alla domanda "is a serial NSoperationQueue FIFO?" -

Se tutte le operazioni hanno la stessa priorità (che non viene modificata dopo che l'operazione è stata aggiunta a una coda) e tutte le operazioni sono sempre - isReady == YES nel momento in cui vengono inserite nella coda delle operazioni, allora una NSOperationQueue seriale è FIFO.

Chris Kane Cocoa Frameworks, Apple

Nel mio caso è possibile sapere quando l'ultima operazione è stata aggiunta alla coda. Quindi, dopo che l'ultima operazione è stata aggiunta, aggiungo un'altra operazione alla coda, di priorità inferiore, che non fa altro che inviare la notifica che la coda è stata svuotata. Data la dichiarazione di Apple, ciò garantisce che venga inviato un solo avviso solo dopo che tutte le operazioni sono state completate.

Se le operazioni vengono aggiunte in un modo che non consente di rilevare l'ultima, (cioè, non deterministica), allora penso che tu debba andare con gli approcci KVO sopra menzionati, con una logica di guardia aggiuntiva aggiunta per provare a rilevare se ulteriormente possono essere aggiunte operazioni.

:)


Salve, sai se e come è possibile essere avvisato quando ogni operazione in coda termina utilizzando un NSOperationQueue con maxConcurrentOperationCount = 1?
Sefran2

@fran: vorrei che le operazioni inviassero una notifica al completamento. In questo modo altri moduli possono registrarsi come osservatori e rispondere al completamento di ciascuno. Se il tuo @selector prende un oggetto di notifica puoi facilmente recuperare l'oggetto che ha pubblicato la notifica, nel caso avessi bisogno di ulteriori dettagli su ciò che è stato appena completato.
software evoluto il

17

Che ne dici di aggiungere un'operazione NSO che dipende da tutte le altre in modo che venga eseguita per ultima?


1
Potrebbe funzionare, ma è una soluzione pesante e sarebbe difficile da gestire se è necessario aggiungere nuove attività alla coda.
Kornel

questa in realtà è molto elegante e quella che ho preferito di più! tu il mio voto.
Yariv Nissim

1
Personalmente questa è la mia soluzione preferita. È possibile creare facilmente una semplice NSBlockOperation per il blocco di completamento che dipende da tutte le altre operazioni.
Puneet Sethi

Potresti riscontrare un problema per cui NSBlockOperation non viene chiamato quando la coda viene annullata. Quindi è necessario eseguire una propria operazione che crea un errore quando viene annullato e chiama un blocco con un parametro di errore.
malhal

Questa è la migliore risposta!
trapper

12

Un'alternativa è usare GCD. Fare riferimento a questo come riferimento.

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

5

Ecco come lo faccio.

Imposta la coda e registrati per le modifiche nella proprietà delle operazioni:

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

... e l'osservatore (in questo caso self) implementa:

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

In questo esempio "spinner" UIActivityIndicatorViewmostra che sta succedendo qualcosa. Ovviamente puoi cambiare a tuo piacimento ...


2
Quel forciclo sembra potenzialmente costoso (cosa succede se annulli tutte le operazioni contemporaneamente? Non otterresti prestazioni quadratiche quando la coda viene ripulita?)
Kornel

Bello, ma attenzione ai thread, perché, secondo la documentazione: "... le notifiche KVO associate a una coda di operazioni possono verificarsi in qualsiasi thread." Probabilmente, dovresti spostare il flusso di esecuzione nella coda delle operazioni principali prima di aggiornare lo spinner
Igor Vasilev

3

Sto usando una categoria per farlo.

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

Utilizzo :

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!)
}];

Fonte: https://gist.github.com/artemstepanenko/7620471


Perché questo file completamento ? Una NSOperationQueue non viene completata: viene semplicemente vuota. Lo stato vuoto può essere immesso più volte durante la durata di un NSOperationQueue.
CouchDeveloper

Questo non funziona se op1 e op2 terminano prima che venga chiamato setCompletion.
malhal

Risposta eccellente, solo 1 avvertenza che il blocco di completamento viene chiamato quando la coda viene completata con l'avvio di tutte le operazioni. Avvio delle operazioni! = Le operazioni sono state completate.
Saqib Saud

Hmm vecchia risposta, ma scommetto che waitUntilFinisheddovrebbe essereYES
brandonscript

3

Come di iOS 13.0 , le proprietà operationCount e operation sono deprecate. È altrettanto semplice tenere traccia del numero di operazioni nella coda e attivare una notifica quando sono state completate tutte. Questo esempio funziona anche con una sottoclasse asincrona di Operation .

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

Di seguito è una sottoclasse di Operazione per operazioni asincrone facili

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.")
}

}


dove viene decrementOperationCount()richiamato il metodo?
iksnae

@iksnae - Ho aggiornato la mia risposta con una sottoclasse di Operazione . Uso decrementOperationCount () all'interno del didSet della mia variabile di stato . Spero che questo ti aiuti!
Caleb Lindsey

2

Che ne dici di usare KVO per osservare la operationCountproprietà della coda? Allora ne sentiresti parlare quando la coda si è svuotata e anche quando ha smesso di essere vuota. Gestire l'indicatore di progresso potrebbe essere semplice come fare qualcosa come:

[indicator setHidden:([queue operationCount]==0)]

Ha funzionato per te? Nella mia domanda il NSOperationQueueda 3.1 si lamenta di non essere conforme a KVO per la chiave operationCount.
Zoul

In realtà non ho provato questa soluzione in un'app, no. Non posso dire se l'OP lo abbia fatto. Ma la documentazione afferma chiaramente che dovrebbe funzionare. Presenterei una segnalazione di bug. developer.apple.com/iphone/library/documentation/Cocoa/…
Sixten Otto

Non esiste alcuna proprietà operationCount su NSOperationQueue nell'SDK di iPhone (almeno non a partire dalla versione 3.1.3). Devi aver guardato la pagina della documentazione di Max OS X ( developer.apple.com/Mac/library/documentation/Cocoa/Reference/… )
Nick Forge

1
Il tempo guarisce tutte le ferite ... e talvolta le risposte sbagliate. A partire da iOS 4, la operationCountproprietà è presente.
Sixten Otto

2

Aggiungi l'ultima operazione come:

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

Così:

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

}

3
quando le attività vengono eseguite contemporaneamente, è un approccio sbagliato.
Marcin

2
E quando la coda viene annullata, quest'ultima operazione non viene nemmeno avviata.
malhal

2

Con ReactiveObjC trovo che funzioni bene:

// 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!");
    }
}];

1

Cordiali saluti, puoi raggiungere questo obiettivo con GCD dispatch_group in swift 3 . Puoi ricevere una notifica quando tutte le attività sono terminate.

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

Qual è la versione iOS minima per utilizzarlo?
Nitesh Borad

È disponibile da swift 3, iOS 8 o versioni successive.
Abhijith

0

Puoi crearne uno nuovo NSThreado eseguire un selettore in background e attendere lì. Al NSOperationQueuetermine, puoi inviare una tua notifica.

Sto pensando a qualcosa come:

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

...

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

Sembra un po 'sciocco creare thread solo per metterlo a dormire.
Kornel

Sono d'accordo. Tuttavia, non riuscivo a trovare un altro modo per aggirarlo.
pgb

Come ti assicureresti che un solo thread sia in attesa? Ho pensato alla bandiera, ma deve essere protetta dalle condizioni di gara, e ho finito per usare troppo NSLock per i miei gusti.
Kornel

Penso che tu possa racchiudere NSOperationQueue in qualche altro oggetto. Ogni volta che metti in coda un'operazione NSO, aumenti un numero e avvii un thread. Ogni volta che un thread finisce, decrementa quel numero di uno. Stavo pensando a uno scenario in cui potresti mettere tutto in coda in anticipo e quindi avviare la coda, quindi avresti bisogno di un solo thread in attesa.
pgb

0

Se utilizzi questa operazione come classe base, potresti passare il whenEmpty {}blocco a OperationQueue :

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

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

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

Il valore del tipo "OperationQueue" non ha membri "whenEmpty"
Dale

@Dale se fai clic sul link, ti ​​porterà a una pagina GitHub dove viene spiegato tutto. Se ricordo bene, la risposta è stata scritta quando OperationQueue della Fondazione era ancora chiamata NSOperationQueue; quindi forse c'era meno ambiguità.
user1244109

Colpa mia ... ho fatto la falsa conclusione che "OperationQueue" sopra era "OperationQueue" di Swift 4.
Dale

0

Senza 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

Se sei arrivato qui alla ricerca di una soluzione con la mietitrebbia, ho finito per ascoltare il mio oggetto di stato.

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

sub = self.$state.sink(receiveValue: { (state) in
 print("state updated: \(state)")
})
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.