Benachrichtigung erhalten, wenn NSOperationQueue alle Aufgaben abgeschlossen hat

92

NSOperationQueuehat waitUntilAllOperationsAreFinished, aber ich möchte nicht synchron darauf warten. Ich möchte nur die Fortschrittsanzeige in der Benutzeroberfläche ausblenden, wenn die Warteschlange beendet ist.

Was ist der beste Weg, um dies zu erreichen?

Ich kann keine Benachrichtigungen von meinem NSOperations senden , da ich nicht weiß, welche die letzte sein wird, und [queue operations]möglicherweise noch nicht leer (oder schlimmer noch - neu gefüllt) ist, wenn eine Benachrichtigung eingeht.

Kornel
quelle
Überprüfen Sie dies, wenn Sie GCD in Swift 3 verwenden. Stackoverflow.com/a/44562935/1522584
Abhijith

Antworten:

166

Verwenden Sie KVO, um die operationsEigenschaft Ihrer Warteschlange zu beobachten. Anschließend können Sie feststellen, ob Ihre Warteschlange vollständig ist, indem Sie nach prüfen [queue.operations count] == 0.

Deklarieren Sie irgendwo in der Datei, in der Sie die KVO ausführen, einen Kontext für KVO wie folgt ( weitere Informationen ):

static NSString *kQueueOperationsChanged = @"kQueueOperationsChanged";

Gehen Sie beim Einrichten Ihrer Warteschlange folgendermaßen vor:

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

Dann machen Sie dies in Ihrem 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];
    }
}

(Dies setzt voraus, dass Sie NSOperationQueuesich in einer Eigenschaft mit dem Namen befinden. queue)

Irgendwann, bevor Ihr Objekt vollständig freigegeben wird (oder wenn es sich nicht mehr um den Warteschlangenstatus kümmert), müssen Sie die Registrierung von KVO wie folgt aufheben:

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


Nachtrag: iOS 4.0 hat eine NSOperationQueue.operationCountEigenschaft, die laut Dokumentation KVO-konform ist. Diese Antwort funktioniert jedoch weiterhin in iOS 4.0, sodass sie für die Abwärtskompatibilität weiterhin nützlich ist.

Nick Forge
quelle
26
Ich würde argumentieren, dass Sie den Property Accessor verwenden sollten, da er eine zukunftssichere Kapselung bietet (wenn Sie sich beispielsweise dazu entschließen, die Warteschlange träge zu initialisieren). Der direkte Zugriff auf eine Immobilie mit dem Ivar kann als vorzeitige Optimierung angesehen werden, hängt jedoch wirklich vom genauen Kontext ab. Die Zeitersparnis durch direkten Zugriff auf eine Immobilie über deren Ivar ist normalerweise vernachlässigbar, es sei denn, Sie verweisen mehr als 100-1000 Mal pro Sekunde auf diese Immobilie (als unglaublich grobe Schätzung).
Nick Forge
2
Versucht, wegen schlechter KVO-Nutzung abzustimmen. Die hier beschriebene ordnungsgemäße Verwendung: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Nikolai Ruhe
19
@NikolaiRuhe Sie haben Recht - die Verwendung dieses Codes bei der Unterklasse einer Klasse, die selbst KVO verwendet, um operationCountdasselbe NSOperationQueueObjekt zu beobachten, würde möglicherweise zu Fehlern führen. In diesem Fall müssten Sie das Kontextargument ordnungsgemäß verwenden. Es ist unwahrscheinlich, aber definitiv möglich. (Das eigentliche Problem zu formulieren ist hilfreicher als das Hinzufügen von Snark + einem Link)
Nick Forge
6
Habe hier eine interessante Idee gefunden . Ich habe das verwendet, um NSOperationQueue zu unterordnen, und eine NSOperation-Eigenschaft 'finalOpearation' hinzugefügt, die als abhängig von jeder der Warteschlange hinzugefügten Operation festgelegt wird. Offensichtlich musste addOperation überschrieben werden: um dies zu tun. Außerdem wurde ein Protokoll hinzugefügt, das nach Abschluss von finalOperation eine Nachricht an einen Delegaten sendet. Hat bisher gearbeitet.
pnizzle
1
Viel besser! Ich freue mich sehr, wenn die Optionen angegeben werden und der Aufruf von removeObserver: von einem @ try / @ catch umbrochen wird. Dies ist nicht ideal, aber in den Apple-Dokumenten wird angegeben, dass beim Aufrufen von removeObserver keine Sicherheit besteht: ... if Das Objekt hat keine Beobachterregistrierung. Die Anwendung stürzt ab.
Austin
20

Wenn Sie etwas erwarten (oder wünschen), das diesem Verhalten entspricht:

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>

Sie sollten sich bewusst sein, dass beim Hinzufügen mehrerer "kurzer" Vorgänge zu einer Warteschlange möglicherweise stattdessen dieses Verhalten angezeigt wird (da Vorgänge als Teil des Hinzufügens zur Warteschlange gestartet werden):

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>

In meinem Projekt musste ich wissen, wann der letzte Vorgang abgeschlossen war, nachdem eine große Anzahl von Vorgängen zu einer seriellen NSOperationQueue hinzugefügt wurde (dh maxConcurrentOperationCount = 1) und erst, wenn alle abgeschlossen waren.

Googeln Ich habe diese Aussage eines Apple-Entwicklers als Antwort auf die Frage "Ist ein serielles NSoperationQueue-FIFO?" Gefunden. - -

Wenn alle Operationen dieselbe Priorität haben (die nicht geändert wird, nachdem die Operation zu einer Warteschlange hinzugefügt wurde) und alle Operationen immer - isReady == YES sind, wenn sie in die Operationswarteschlange gestellt werden, ist eine serielle NSOperationQueue FIFO.

Chris Kane Cocoa Frameworks, Apple

In meinem Fall ist es möglich zu wissen, wann der letzte Vorgang zur Warteschlange hinzugefügt wurde. Nachdem der letzte Vorgang hinzugefügt wurde, füge ich der Warteschlange einen weiteren Vorgang mit niedrigerer Priorität hinzu, der lediglich die Benachrichtigung sendet, dass die Warteschlange geleert wurde. Aufgrund der Aussage von Apple wird so sichergestellt, dass nur eine einzige Benachrichtigung gesendet wird, nachdem alle Vorgänge abgeschlossen wurden.

Wenn Operationen auf eine Weise hinzugefügt werden, die es nicht erlaubt, die letzte zu erkennen (dh nicht deterministisch), dann müssen Sie meiner Meinung nach mit den oben genannten KVO-Ansätzen fortfahren und zusätzliche Schutzlogik hinzufügen, um zu versuchen, weitere zu erkennen Operationen können hinzugefügt werden.

:) :)

Software entwickelt
quelle
Hallo, wissen Sie, ob und wie Sie benachrichtigt werden können, wenn jede Operation in der Warteschlange endet, indem Sie eine NSOperationQueue mit maxConcurrentOperationCount = 1 verwenden?
Sefran2
@fran: Ich würde die Operationen nach Abschluss eine Benachrichtigung veröffentlichen lassen. Auf diese Weise können sich andere Module als Beobachter registrieren und nach Abschluss jedes Moduls antworten. Wenn Ihr @selector ein Benachrichtigungsobjekt verwendet, können Sie das Objekt, das die Benachrichtigung veröffentlicht hat, problemlos abrufen, falls Sie weitere Details zu der gerade abgeschlossenen Operation benötigen.
Software entwickelt
17

Wie wäre es mit dem Hinzufügen einer NSOperation, die von allen anderen abhängig ist, damit sie zuletzt ausgeführt wird?

Meistens ja
quelle
1
Es könnte funktionieren, aber es ist eine Schwergewichtslösung, und es wäre mühsam zu verwalten, wenn Sie der Warteschlange neue Aufgaben hinzufügen müssen.
Kornel
das ist eigentlich sehr elegant und das, das ich am meisten bevorzugt habe! du meine Stimme.
Yariv Nissim
1
Persönlich ist dies meine Lieblingslösung. Sie können einfach eine einfache NSBlockOperation für den Abschlussblock erstellen, die von allen anderen Vorgängen abhängt.
Puneet Sethi
Möglicherweise tritt ein Problem auf, dass NSBlockOperation nicht aufgerufen wird, wenn die Warteschlange abgebrochen wird. Sie müssen also eine eigene Operation ausführen, die beim Abbrechen einen Fehler erzeugt und einen Block mit einem Fehlerparameter aufruft.
Malhal
Das ist die beste Antwort!
Trapper
12

Eine Alternative ist die Verwendung von GCD. Siehe diese als Referenz.

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
quelle
5

So mache ich es.

Richten Sie die Warteschlange ein und registrieren Sie sich für Änderungen in der Operationseigenschaft:

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

... und der Beobachter (in diesem Fall self) implementiert:

- (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 diesem Beispiel zeigt "Spinner" UIActivityIndicatorView, dass etwas passiert. Natürlich können Sie nach Bedarf wechseln ...

Kris Jenkins
quelle
2
Diese forSchleife scheint potenziell teuer zu sein (was ist, wenn Sie alle Vorgänge auf einmal abbrechen? Würde das nicht eine quadratische Leistung bringen, wenn die Warteschlange bereinigt wird?)
Kornel
Schön, aber seien Sie vorsichtig mit Threads, denn laut Dokumentation: "... KVO-Benachrichtigungen, die einer Operationswarteschlange zugeordnet sind, können in jedem Thread auftreten." Wahrscheinlich müssten Sie den Ausführungsfluss in die Hauptoperationswarteschlange verschieben, bevor Sie den Spinner aktualisieren
Igor Vasilev
3

Ich benutze dazu eine Kategorie.

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

Verwendung :

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

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

Brandonscript
quelle
Warum ist das eine Fertigstellung ? Eine NSOperationQueue wird nicht abgeschlossen - sie wird lediglich leer. Der leere Status kann während der Lebensdauer einer NSOperationQueue mehrmals eingegeben werden.
CouchDeveloper
Dies funktioniert nicht, wenn op1 und op2 vor dem Aufruf von setCompletion beendet werden.
Malhal
Hervorragende Antwort, nur 1 Einschränkung, dass der Abschlussblock aufgerufen wird, wenn die Warteschlange mit dem Starten des gesamten Vorgangs abgeschlossen ist. Startvorgänge! = Vorgänge sind abgeschlossen.
Saqib Saud
Hmm alte Antwort, aber ich wette, waitUntilFinishedsollte seinYES
Brandonscript
3

Ab iOS 13.0 sind die Eigenschaften operationCount und operation veraltet. Es ist genauso einfach, die Anzahl der Vorgänge in Ihrer Warteschlange selbst zu verfolgen und eine Benachrichtigung auszulösen, wenn alle abgeschlossen sind. Dieses Beispiel funktioniert auch mit einer asynchronen Unterklasse von 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
    }
}

Unten finden Sie eine Unterklasse von Operation für einfache asynchrone Operationen

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
quelle
Wo wird die decrementOperationCount()Methode aufgerufen?
Iksnae
@iksnae - Ich habe meine Antwort mit einer Unterklasse von Operation aktualisiert . Ich benutze decrementOperationCount () im didSet meines Zustand variabel. Hoffe das hilft!
Caleb Lindsey
2

Was ist mit KVO, um die operationCountEigenschaft der Warteschlange zu beobachten ? Dann würden Sie davon hören, wenn die Warteschlange leer wurde und auch wenn sie nicht mehr leer war. Der Umgang mit der Fortschrittsanzeige kann so einfach sein wie:

[indicator setHidden:([queue operationCount]==0)]
Sixten Otto
quelle
Hat das für dich funktioniert? In meiner Anwendung NSOperationQueuebeschwert sich der ab 3.1, dass er für den Schlüssel nicht KVO-konform ist operationCount.
Zoul
Ich habe diese Lösung nicht in einer App ausprobiert, nein. Ich kann nicht sagen, ob das OP es getan hat. In der Dokumentation heißt es jedoch eindeutig, dass es funktionieren sollte . Ich würde einen Fehlerbericht einreichen. developer.apple.com/iphone/library/documentation/Cocoa/…
Sixten Otto
Es gibt keine operationCount-Eigenschaft für NSOperationQueue im iPhone SDK (zumindest nicht ab 3.1.3). Sie müssen sich die Max OS X-Dokumentationsseite angesehen haben ( developer.apple.com/Mac/library/documentation/Cocoa/Reference/… )
Nick Forge,
1
Die Zeit heilt alle Wunden ... und manchmal falsche Antworten. Ab iOS 4 ist die operationCountEigenschaft vorhanden.
Sixten Otto
2

Fügen Sie die letzte Operation wie folgt hinzu:

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

So:

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

}
pvllnspk
quelle
3
Wenn Aufgaben gleichzeitig ausgeführt werden, ist dies ein falscher Ansatz.
Marcin
2
Und wenn die Warteschlange abgebrochen wird, wird dieser letzte Vorgang noch nicht einmal gestartet.
Malhal
2

Mit ReactiveObjC finde ich, dass dies gut funktioniert:

// 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
quelle
1

Zu Ihrer Information, Sie können dies mit GCD dispatch_group in Swift 3 erreichen . Sie können benachrichtigt werden, wenn alle Aufgaben abgeschlossen sind.

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
quelle
Was ist die minimale iOS-Version, um dies zu verwenden?
Nitesh Borad
Es ist ab Swift 3, iOS 8 oder höher verfügbar.
Abhijith
0

Sie können eine neue erstellen NSThreadoder eine Auswahl im Hintergrund ausführen und dort warten. Wenn der NSOperationQueueVorgang abgeschlossen ist, können Sie eine eigene Benachrichtigung senden.

Ich denke an etwas wie:

- (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
quelle
Es scheint ein bisschen albern, einen Thread zu erstellen, nur um ihn in den Ruhezustand zu versetzen.
Kornel
Genau. Trotzdem konnte ich keinen anderen Weg finden.
pgb
Wie würden Sie sicherstellen, dass nur ein Thread wartet? Ich habe über die Flagge nachgedacht, aber das muss vor Rennbedingungen geschützt werden, und am Ende habe ich zu viel NSLock für meinen Geschmack verwendet.
Kornel
Ich denke, Sie können die NSOperationQueue in ein anderes Objekt einschließen. Immer wenn Sie eine NSOperation in die Warteschlange stellen, erhöhen Sie eine Zahl und starten einen Thread. Immer wenn ein Thread endet, verringern Sie diese Zahl um eins. Ich dachte an ein Szenario, in dem Sie alles vorher in die Warteschlange stellen und dann die Warteschlange starten könnten, sodass Sie nur einen wartenden Thread benötigen würden.
pgb
0

Wenn Sie diese Operation als Basisklasse verwenden, können Sie den whenEmpty {}Block an die OperationQueue übergeben :

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

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

queue.whenEmpty = {
    print("all operations finished")
}
user1244109
quelle
Der Wert vom Typ 'OperationQueue' hat kein Mitglied 'whenEmpty'
Dale
@Dale Wenn Sie auf den Link klicken, gelangen Sie zu einer Github-Seite, auf der alles erklärt wird. Wenn ich mich richtig erinnere, wurde die Antwort geschrieben, als die OperationQueue der Foundation noch NSOperationQueue hieß. so gab es vielleicht weniger Zweideutigkeit.
user1244109
Mein schlechtes ... Ich bin zu dem falschen Schluss gekommen, dass die "OperationQueue" oben die "OperationQueue" von Swift 4 war.
Dale
0

Ohne 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
quelle
0

Wenn Sie hier nach einer Lösung mit Mähdrescher gesucht haben, habe ich mir nur mein eigenes Statusobjekt angehört.

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

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