NSOperationQueue
ma 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.
Odpowiedzi:
Użyj KVO do obserwowania
operations
wł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
NSOperationQueue
znajdujesz się w nieruchomości o nazwiequeue
)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.operationCount
wł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.źródło
operationCount
tego samegoNSOperationQueue
obiektu, 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)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?” -
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.
:)
źródło
Co powiesz na dodanie operacji NSO, która jest zależna od wszystkich innych, więc będzie działać jako ostatnia?
źródło
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 });
źródło
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
UIActivityIndicatorView
pokazanie, że coś się dzieje. Oczywiście możesz zmienić swój styl ...źródło
for
pętla wydaje się potencjalnie kosztowna (co, jeśli anulujesz wszystkie operacje naraz? Czy nie dałoby to kwadratowej wydajności, gdy kolejka jest czyszczona?)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.") }
}
źródło
decrementOperationCount()
wywoływana jest metoda?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
źródło
waitUntilFinished
powinno byćYES
A co z wykorzystaniem KVO do obserwowania
operationCount
wł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)]
źródło
NSOperationQueue
od 3.1 narzeka, że nie jest zgodny z KVO dla kluczaoperationCount
.operationCount
właściwość jest obecna.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]; }
źródło
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!"); } }];
źródło
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") }
źródło
Możesz utworzyć nowy
NSThread
lub uruchomić selektor w tle i tam czekać. PoNSOperationQueue
zakoń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"]; }
źródło
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") }
źródło
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) } }
źródło
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)") })
źródło