Otrzymuj powiadomienia, gdy NSOperationQueue zakończy wszystkie zadania

93

NSOperationQueuema waitUntilAllOperationsAreFinished, ale nie chcę na to czekać synchronicznie. Chcę tylko ukryć wskaźnik postępu w interfejsie użytkownika po zakończeniu kolejki.

Jaki jest najlepszy sposób na osiągnięcie tego?

Nie mogę wysyłać powiadomień ze swoich NSOperation, ponieważ nie wiem, które z nich będzie ostatnie i [queue operations]może jeszcze nie być puste (lub co gorsza - ponownie zapełnione), gdy otrzymam powiadomienie.

Kornel
źródło
Zaznacz to, jeśli używasz GCD w swift 3. stackoverflow.com/a/44562935/1522584
Abhijith

Odpowiedzi:

167

Użyj KVO do obserwowania operationswłaściwości swojej kolejki, a następnie możesz stwierdzić, czy kolejka została zakończona, sprawdzając [queue.operations count] == 0.

Gdzieś w pliku, w którym robisz KVO, zadeklaruj kontekst dla KVO w następujący sposób ( więcej informacji ):

static NSString *kQueueOperationsChanged = @"kQueueOperationsChanged";

Podczas konfigurowania kolejki wykonaj następujące czynności:

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

Następnie zrób to w swoim 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];
    }
}

(Przy założeniu, że NSOperationQueueznajdujesz się w nieruchomości o nazwie queue)

W pewnym momencie, zanim Twój obiekt całkowicie zwolni (lub gdy przestanie dbać o stan kolejki), będziesz musiał wyrejestrować się z KVO w następujący sposób:

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


Dodatek: iOS 4.0 posiada NSOperationQueue.operationCountwłaściwość, która zgodnie z dokumentacją jest zgodna z KVO. Ta odpowiedź będzie jednak nadal działać w iOS 4.0, więc nadal jest przydatna do wstecznej kompatybilności.

Nick Forge
źródło
26
Twierdziłbym, że powinieneś użyć akcesor właściwości, ponieważ zapewnia on przyszłościową enkapsulację (jeśli zdecydujesz się np. Na leniwe zainicjowanie kolejki). Bezpośredni dostęp do właściwości przez jej ivar można uznać za przedwczesną optymalizację, ale tak naprawdę zależy to od dokładnego kontekstu. Czas zaoszczędzony przez bezpośredni dostęp do nieruchomości przez jej ivar jest zwykle znikomy, chyba że odwołujesz się do tej nieruchomości więcej niż 100-1000 razy na sekundę (jako niewiarygodnie przybliżony szacunek).
Nick Forge
2
Kuszony do przegłosowania z powodu złego użycia KVO. Prawidłowe użycie opisane tutaj: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Nikolai Ruhe
19
@NikolaiRuhe Masz rację - użycie tego kodu podczas tworzenia podklas klasy, która sama używa KVO do obserwacji operationCounttego samego NSOperationQueueobiektu, mogłoby potencjalnie doprowadzić do błędów, w takim przypadku należałoby poprawnie użyć argumentu context. To mało prawdopodobne, ale zdecydowanie możliwe. (Określenie rzeczywistego problemu jest bardziej pomocne niż dodanie snarka + link)
Nick Forge,
6
Znalazłem tutaj ciekawy pomysł . Użyłem tego do podklasy NSOperationQueue, dodałem właściwość NSOperation „finalOpearation”, która jest ustawiana jako zależna od każdej operacji dodanej do kolejki. Oczywiście musiał zastąpić addOperation: aby to zrobić. Dodano również protokół, który wysyła wiadomość do delegata po zakończeniu finalOperation. Pracuje do tej pory.
pnizzle
1
Dużo lepiej! Będę najbardziej szczęśliwy, gdy opcje zostaną określone, a wywołanie removeObserver: jest opakowane przez @ try / @ catch - To nie jest idealne, ale dokumentacja Apple określa, że ​​nie ma bezpieczeństwa podczas wywoływania removeObserver: ... if obiekt nie posiada rejestracji obserwatora, aplikacja ulegnie awarii.
Austin
20

Jeśli spodziewasz się (lub pragniesz) czegoś, co pasuje do tego zachowania:

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>

Należy mieć świadomość, że jeśli do kolejki dodaje się kilka „krótkich” operacji, można zamiast tego zobaczyć takie zachowanie (ponieważ operacje są uruchamiane jako część dodawania do kolejki):

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>

W moim projekcie musiałem wiedzieć, kiedy zakończyła się ostatnia operacja, po dodaniu dużej liczby operacji do szeregowego NSOperationQueue (tj. MaxConcurrentOperationCount = 1) i tylko wtedy, gdy wszystkie zostały zakończone.

Googling Znalazłem to oświadczenie od programisty Apple w odpowiedzi na pytanie „czy serial NSoperationQueue FIFO?” -

Jeśli wszystkie operacje mają ten sam priorytet (który nie jest zmieniany po dodaniu operacji do kolejki) i wszystkie operacje są zawsze - isReady == TAK do czasu ich umieszczenia w kolejce operacji, wówczas szeregową kolejką NSOperationQueue jest FIFO.

Chris Kane Cocoa Frameworks, Apple

W moim przypadku można wiedzieć, kiedy ostatnia operacja została dodana do kolejki. Więc po dodaniu ostatniej operacji dodaję do kolejki kolejną operację o niższym priorytecie, która nic nie robi poza wysłaniem powiadomienia, że ​​kolejka została opróżniona. Biorąc pod uwagę oświadczenie Apple, zapewnia to, że tylko jedno powiadomienie zostanie wysłane dopiero po zakończeniu wszystkich operacji.

Jeśli operacje są dodawane w sposób, który nie pozwala na wykrycie ostatniego (tj. Niedeterministyczny), to myślę, że musisz iść z podejściami KVO wspomnianymi powyżej, z dodatkową logiką ochronną, aby spróbować wykryć, czy dalej operacje mogą zostać dodane.

:)

oprogramowanie ewoluowało
źródło
Cześć, czy wiesz, czy i jak można otrzymać powiadomienie o zakończeniu każdej operacji w kolejce przy użyciu NSOperationQueue z wartością maxConcurrentOperationCount = 1?
Sefran2
@fran: Chciałbym, aby operacje opublikowały powiadomienie po zakończeniu. W ten sposób inne moduły mogą zarejestrować się jako obserwatorzy i odpowiadać po zakończeniu każdego z nich. Jeśli Twój @selector przyjmuje obiekt powiadomienia, możesz łatwo pobrać obiekt, który opublikował powiadomienie, na wypadek, gdybyś potrzebował dalszych informacji o tym, co właśnie zostało zakończone.
oprogramowanie ewoluowało
17

Co powiesz na dodanie operacji NSO, która jest zależna od wszystkich innych, więc będzie działać jako ostatnia?

W większości tak
źródło
1
To może działać, ale jest to ciężkie rozwiązanie i byłoby trudne w zarządzaniu, gdyby trzeba było dodać nowe zadania do kolejki.
Kornel
to jest naprawdę bardzo eleganckie i najbardziej mi się podobało! ty mój głos.
Yariv Nissim
1
Osobiście to moje ulubione rozwiązanie. Możesz łatwo utworzyć prostą NSBlockOperation dla bloku uzupełniania, który zależy od wszystkich innych operacji.
Puneet Sethi
Może wystąpić problem polegający na tym, że NSBlockOperation nie jest wywoływana po anulowaniu kolejki. Musisz więc wykonać własną operację, która po anulowaniu tworzy błąd i wywołuje blok z parametrem błędu.
malhal
To najlepsza odpowiedź!
trapper
12

Jedną z możliwości jest użycie GCD. Odnieś się do tego jako odniesienia.

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
źródło
5

Tak to robię.

Skonfiguruj kolejkę i zarejestruj się pod kątem zmian właściwości operacji:

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

... a obserwator (w tym przypadku self) realizuje:

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

W tym przykładzie „spinner” to UIActivityIndicatorViewpokazanie, że coś się dzieje. Oczywiście możesz zmienić swój styl ...

Kris Jenkins
źródło
2
Ta forpętla wydaje się potencjalnie kosztowna (co, jeśli anulujesz wszystkie operacje naraz? Czy nie dałoby to kwadratowej wydajności, gdy kolejka jest czyszczona?)
Kornel
Fajny, ale uważaj na wątki, bo zgodnie z dokumentacją: "... powiadomienia KVO związane z kolejką operacji mogą wystąpić w dowolnym wątku." Prawdopodobnie należałoby przenieść przepływ wykonywania do głównej kolejki operacji przed aktualizacją przędzarki
Igor Vasilev
4

Od wersji iOS 13.0 właściwości operationCount i operation są przestarzałe. Równie proste jest samodzielne śledzenie liczby operacji w kolejce i wysyłanie Powiadomień po ich zakończeniu. Ten przykład działa również z asynchroniczną podklasą 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
    }
}

Poniżej znajduje się podklasa Operation dla łatwych operacji asynchronicznych

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
źródło
gdzie decrementOperationCount()wywoływana jest metoda?
iksnae
@iksnae - zaktualizowałem moją odpowiedź o podklasę operacji . Używam decmentOperationCount () w ramach didSet mojej zmiennej stanu . Mam nadzieję że to pomoże!
Caleb Lindsey
3

Używam do tego kategorii.

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

Zastosowanie :

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

Źródło: https://gist.github.com/artemstepanenko/7620471

brandonscript
źródło
Dlaczego to jest ukończenie ? NSOperationQueue nie kończy się - po prostu staje się puste. Stan pusty można wprowadzić kilka razy w okresie istnienia NSOperationQueue.
CouchDeveloper,
To nie działa, jeśli op1 i op2 kończą się przed wywołaniem setCompletion.
malhal
Doskonała odpowiedź, tylko jedno zastrzeżenie, że blok uzupełniania zostanie wywołany, gdy kolejka zostanie zakończona wraz z rozpoczęciem całej operacji. Uruchamianie operacji! = Operacje są zakończone.
Saqib Saud,
Hmm stara odpowiedź, ale założę się, że waitUntilFinishedpowinno byćYES
brandonscript
2

A co z wykorzystaniem KVO do obserwowania operationCountwłaściwości kolejki? Wtedy słyszałeś o tym, gdy kolejka się opróżniła, a także gdy przestała być pusta. Radzenie sobie ze wskaźnikiem postępu może być tak proste, jak zrobienie czegoś takiego:

[indicator setHidden:([queue operationCount]==0)]
Sixten Otto
źródło
Czy to zadziałało dla Ciebie? W mojej aplikacji NSOperationQueueod 3.1 narzeka, że ​​nie jest zgodny z KVO dla klucza operationCount.
zoul
Nie próbowałem tego rozwiązania w aplikacji, nie. Nie mogę powiedzieć, czy OP to zrobił. Ale dokumentacja jasno stwierdza, że powinno działać. Złożyłbym raport o błędzie. developer.apple.com/iphone/library/documentation/Cocoa/…
Sixten Otto
Nie ma właściwości operationCount w NSOperationQueue w zestawie iPhone SDK (przynajmniej nie w wersji 3.1.3). Musiałeś przeglądać stronę dokumentacji Max OS X ( developer.apple.com/Mac/library/documentation/Cocoa/Reference/… )
Nick Forge
1
Czas leczy wszystkie rany ... a czasem błędne odpowiedzi. Od iOS 4 operationCountwłaściwość jest obecna.
Sixten Otto
2

Dodaj ostatnią operację, taką jak:

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

Więc:

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

}
pvllnspk
źródło
3
gdy zadania są wykonywane jednocześnie, jest to niewłaściwe podejście.
Marcin
2
A kiedy kolejka jest anulowana, ta ostatnia operacja nawet się nie rozpoczyna.
malhal
2

Z ReactiveObjC uważam, że działa to ładnie:

// 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!");
    }
}];
Szlagier
źródło
1

FYI, możesz to osiągnąć dzięki GCD dispatch_group w Swift 3 . Możesz otrzymywać powiadomienia o zakończeniu wszystkich zadań.

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
źródło
Jaka jest minimalna wersja iOS, aby z tego korzystać?
Nitesh Borad
Jest dostępny od Swift 3, iOS 8 lub nowszego.
Abhijith
0

Możesz utworzyć nowy NSThreadlub uruchomić selektor w tle i tam czekać. Po NSOperationQueuezakończeniu możesz wysłać własne powiadomienie.

Myślę o czymś takim:

- (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
źródło
Tworzenie nici tylko po to, by ją uśpić, wydaje się trochę głupie.
Kornel
Zgadzam się. Mimo to nie mogłem znaleźć innego sposobu obejścia tego.
pgb
Jak możesz upewnić się, że czeka tylko jeden wątek? Myślałem o fladze, ale to musi być chronione przed warunkami wyścigu i skończyło się na tym, że użyłem zbyt dużo NSLock jak na mój gust.
Kornel
Myślę, że możesz zawinąć NSOperationQueue w jakiś inny obiekt. Za każdym razem, gdy kolejkujesz operację NSO, zwiększasz liczbę i uruchamiasz wątek. Za każdym razem, gdy wątek się kończy, zmniejszasz tę liczbę o jeden. Zastanawiałem się nad scenariuszem, w którym można by ustawić wszystko wcześniej w kolejce, a następnie rozpocząć kolejkę, więc potrzebowałbyś tylko jednego wątku oczekującego.
pgb
0

Jeśli używasz tej operacji jako klasy bazowej, możesz przekazać whenEmpty {}blok do 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
źródło
1
Wartość typu „OperationQueue” nie ma elementu członkowskiego „whenEmpty”
Dale
@Dale, jeśli klikniesz link, przeniesie Cię do strony github, na której wszystko jest wyjaśnione. Jeśli dobrze pamiętam, odpowiedź została napisana, gdy OperationQueue Fundacji wciąż nazywało się NSOperationQueue; więc może było mniej niejasności.
user1244109
Mój błąd ... Doszedłem do fałszywego wniosku, że „OperationQueue” powyżej to „OperationQueue” języka Swift 4.
Dale
0

Bez 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
źródło
0

Jeśli trafiłeś tutaj, szukając rozwiązania z kombajnem - skończyło się na tym, że słuchałem własnego obiektu stanu.

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

sub = self.$state.sink(receiveValue: { (state) in
 print("state updated: \(state)")
})
afański
źródło