Получать уведомление, когда NSOperationQueue завершает все задачи

93

NSOperationQueueесть waitUntilAllOperationsAreFinished, но я не хочу его синхронно ждать. Я просто хочу скрыть индикатор прогресса в пользовательском интерфейсе, когда очередь заканчивается.

Как лучше всего этого добиться?

Я не могу отправлять уведомления с моего NSOperations, потому что я не знаю, какое из них будет последним, и, [queue operations]возможно, еще не пусто (или, что еще хуже, повторно заполнено), когда уведомление получено.

Корнель
источник
Проверьте это, если вы используете GCD в
Swift

Ответы:

167

Используйте KVO для наблюдения за operationsсвойством вашей очереди, а затем вы можете узнать, завершилась ли ваша очередь, проверив [queue.operations count] == 0.

Где-нибудь в файле, в котором вы выполняете KVO, объявите контекст для KVO следующим образом ( подробнее ):

static NSString *kQueueOperationsChanged = @"kQueueOperationsChanged";

Когда вы настраиваете свою очередь, сделайте следующее:

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

Затем сделайте это в своем 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];
    }
}

(Предполагается, что вы NSOperationQueueнаходитесь в собственности с именем queue)

В какой-то момент до того, как ваш объект полностью освободится (или когда он перестанет заботиться о состоянии очереди), вам нужно отменить регистрацию в KVO следующим образом:

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


Приложение: iOS 4.0 имеет NSOperationQueue.operationCountсвойство, которое, согласно документации, является KVO-совместимым. Однако этот ответ по-прежнему будет работать в iOS 4.0, поэтому он по-прежнему полезен для обратной совместимости.

Ник Фордж
источник
26
Я бы сказал, что вам следует использовать аксессор свойств, поскольку он обеспечивает инкапсуляцию с учетом требований будущего (если вы решите, например, лениво инициализировать очередь). Прямой доступ к свойству с помощью его ivar можно рассматривать как преждевременную оптимизацию, но это действительно зависит от точного контекста. Время, сэкономленное за счет прямого доступа к свойству через его ivar, обычно будет незначительным, если только вы не ссылаетесь на это свойство более 100-1000 раз в секунду (как невероятно грубая оценка).
Ник Фордж
2
Соблазн проголосовать против из-за плохого использования KVO. Правильное использование описано здесь: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Николай Рухе
19
@NikolaiRuhe Вы правы - использование этого кода при создании подкласса класса, который сам использует KVO для наблюдения operationCountза одним и тем же NSOperationQueueобъектом, потенциально может привести к ошибкам, и в этом случае вам нужно будет правильно использовать аргумент контекста. Это вряд ли произойдет, но определенно возможно. (Объяснение реальной проблемы более полезно, чем добавление snark + ссылка)
Ник Фордж
6
Нашел интересную идею здесь . Я использовал это для создания подкласса NSOperationQueue, добавил свойство NSOperation, 'finalOpearation', которое устанавливается в зависимости от каждой операции, добавленной в очередь. Очевидно, для этого пришлось переопределить addOperation :. Также добавлен протокол, который отправляет сообщение делегату после завершения finalOperation. Работает до сих пор.
pnizzle
1
Намного лучше! Я буду очень счастлив, если будут указаны параметры и вызов removeObserver: завершится @ try / @ catch - это не идеально, но в документации Apple указано, что при вызове removeObserver: ... if объект не имеет регистрации наблюдателя, приложение выйдет из строя.
Остин
20

Если вы ожидаете (или желаете) чего-то, что соответствует этому поведению:

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>

Вы должны знать, что если в очередь добавляется несколько «коротких» операций, вы можете вместо этого увидеть это поведение (поскольку операции запускаются как часть добавления в очередь):

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>

В моем проекте мне нужно было знать, когда завершилась последняя операция, после того, как большое количество операций было добавлено в серийный NSOperationQueue (т. Е. MaxConcurrentOperationCount = 1) и только тогда, когда все они были завершены.

Погуглил. Я нашел это заявление от разработчика Apple в ответ на вопрос "является ли серийный NSoperationQueue FIFO?" -

Если все операции имеют одинаковый приоритет (который не изменяется после добавления операции в очередь) и все операции всегда - isReady == YES к тому времени, когда они помещаются в очередь операций, то серийный NSOperationQueue равен FIFO.

Крис Кейн Какао Frameworks, Apple

В моем случае можно узнать, когда последняя операция была добавлена ​​в очередь. Поэтому после добавления последней операции я добавляю в очередь еще одну операцию с более низким приоритетом, которая ничего не делает, кроме отправки уведомления о том, что очередь была очищена. Учитывая заявление Apple, это гарантирует, что только одно уведомление будет отправлено только после завершения всех операций.

Если операции добавляются таким образом, который не позволяет обнаружить последний (т.е. недетерминированный), то я думаю, вам следует использовать подходы KVO, упомянутые выше, с добавлением дополнительной логики защиты, чтобы попытаться определить, если еще операции могут быть добавлены.

:)

программное обеспечение развивалось
источник
Привет, знаете ли вы, можно ли и как получить уведомление о завершении каждой операции в очереди с помощью NSOperationQueue с maxConcurrentOperationCount = 1?
Sefran2
@fran: Я бы хотел, чтобы по завершении операции были отправлены уведомления. Таким образом, другие модули могут регистрироваться как наблюдатели и отвечать по мере завершения каждого из них. Если ваш @selector принимает объект уведомления, вы можете легко получить объект, отправивший уведомление, на случай, если вам понадобятся дополнительные сведения о том, какая операция только что завершилась.
программное обеспечение появилось
17

Как насчет добавления NSOperation, который зависит от всех остальных, чтобы он работал последним?

В основном да
источник
1
Это может сработать, но это тяжеловесное решение, и им будет сложно управлять, если вам нужно добавить новые задачи в очередь.
Kornel
на самом деле это очень элегантно, и я предпочел его больше всего! ты мой голос.
Ярив Ниссим
1
Лично это мое любимое решение. Вы можете легко создать простую NSBlockOperation для блока завершения, который зависит от всех других операций.
Пунит Сетхи,
Вы можете столкнуться с проблемой, что NSBlockOperation не вызывается при отмене очереди. Поэтому вам нужно сделать свою собственную операцию, которая при отмене создает ошибку и вызывает блок с параметром ошибки.
malhal
Это лучший ответ!
траппер
12

Одна альтернатива - использовать GCD. См. Это как ссылку.

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

Вот как я это делаю.

Настройте очередь и зарегистрируйтесь для изменений в свойстве операций:

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

... и наблюдатель (в данном случае self) реализует:

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

В этом примере «прядильщик» UIActivityIndicatorViewпоказывает, что что-то происходит. Очевидно, вы можете переодеться ...

Крис Дженкинс
источник
2
Этот forцикл кажется потенциально дорогим (что, если вы отмените все операции сразу? Разве это не приведет к квадратичной производительности при очистке очереди?)
Корнель
Хороший вариант, но будьте осторожны с потоками, потому что, согласно документации: «... Уведомления KVO, связанные с очередью операций, могут возникать в любом потоке». Возможно, вам нужно будет переместить поток выполнения в основную очередь операций, прежде чем обновлять счетчик
Игорь Васильев
4

Начиная с iOS 13.0 , свойства operationCount и operation не рекомендуются. Так же просто самостоятельно отслеживать количество операций в очереди и запускать уведомление, когда все они будут выполнены. Этот пример также работает с асинхронным подклассом 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
    }
}

Ниже приведен подкласс Operation для простых асинхронных операций.

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

}

Калеб Линдси
источник
где вызывается decrementOperationCount()метод?
iksnae
@iksnae - Я обновил свой ответ подклассом Operation . Я использую DecmentOperationCount () внутри didSet моей переменной состояния . Надеюсь это поможет!
Калеб Линдси,
3

Для этого я использую категорию.

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

Использование :

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

Источник: https://gist.github.com/artemstepanenko/7620471

брендонскрипт
источник
Почему это завершение ? NSOperationQueue не завершается - он просто становится пустым. В пустое состояние можно войти несколько раз в течение времени существования NSOperationQueue.
CouchDeveloper
Это не работает, если op1 и op2 заканчиваются до вызова setCompletion.
malhal
Отличный ответ, только одно предостережение, что блок завершения вызывается, когда очередь завершается с запуском всей операции. Запуск операций! = Операции завершены.
Saqib Saud
Хм, старый ответ, но готов поспорить, waitUntilFinishedдолжен бытьYES
brandonscript
2

Как насчет использования KVO для наблюдения за operationCountсвойством очереди? Тогда вы услышите об этом, когда очередь станет пустой, а также когда она перестанет быть пустой. Работать с индикатором прогресса может быть так же просто, как просто сделать что-то вроде:

[indicator setHidden:([queue operationCount]==0)]
Сикстен Отто
источник
Это сработало для вас? В моем приложении NSOperationQueueверсия 3.1 жалуется, что она не совместима с KVO для ключа operationCount.
zoul
На самом деле я не пробовал это решение в приложении, нет. Не могу сказать, сделал ли OP. Но в документации четко сказано, что это должно работать. Я бы отправил отчет об ошибке. developer.apple.com/iphone/library/documentation/Cocoa/…
Сикстен Отто
В iPhone SDK отсутствует свойство operationCount для NSOperationQueue (по крайней мере, не начиная с версии 3.1.3). Вы, должно быть, просматривали страницу документации Max OS X ( developer.apple.com/Mac/library/documentation/Cocoa/Reference/… )
Ник Фордж
1
Время лечит все раны ... а иногда и неправильные ответы. Начиная с iOS 4 operationCountсвойство присутствует.
Сикстен Отто
2

Добавьте последнюю операцию, например:

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

Так:

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

}
pvllnspk
источник
3
когда задачи выполняются одновременно, это неправильный подход.
Marcin
2
И когда очередь отменяется, эта последняя операция даже не запускается.
malhal
2

Я считаю, что с ReactiveObjC это прекрасно работает:

// 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
источник
1

К вашему сведению, вы можете добиться этого с помощью GCD dispatch_group в swift 3 . Вы можете получать уведомления, когда все задачи будут выполнены.

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")
}
Абхиджит
источник
Какая минимальная версия iOS для этого?
Нитеш Борад
Он доступен в Swift 3, iOS 8 или выше.
Abhijith
0

Вы можете создать новый NSThreadили выполнить селектор в фоновом режиме и подождать там. По NSOperationQueueзавершении вы можете отправить собственное уведомление.

Я думаю о чем-то вроде:

- (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
источник
Кажется немного глупым создавать поток только для того, чтобы усыпить его.
Kornel
Согласен. Тем не менее, я не мог найти другого выхода.
pgb
Как убедиться, что ожидает только один поток? Я думал о флаге, но он должен быть защищен от условий гонки, и в итоге я использовал слишком много NSLock на свой вкус.
Kornel
Я думаю, вы можете обернуть NSOperationQueue в какой-то другой объект. Всякий раз, когда вы ставите NSOperation в очередь, вы увеличиваете число и запускаете поток. Каждый раз, когда поток заканчивается, вы уменьшаете это число на единицу. Я думал о сценарии, в котором вы могли бы поставить все в очередь заранее, а затем запустить очередь, так что вам понадобится только один ожидающий поток.
pgb
0

Если вы используете эту операцию в качестве базового класса, вы можете передать whenEmpty {}блок в 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
источник
1
Значение типа OperationQueue не имеет члена whenEmpty
Дейл
@Dale, если вы нажмете на ссылку, вы попадете на страницу github, где все объяснено. Если я правильно помню, ответ был написан, когда OperationQueue Foundation все еще назывался NSOperationQueue; так что, возможно, было меньше двусмысленности.
user1244109
Мое плохое ... Я сделал ложный вывод, что "OperationQueue" выше была "OperationQueue" Swift 4.
Dale
0

Без КВО

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

Если вы пришли сюда в поисках решения с помощью комбайна - в итоге я просто слушал свой собственный объект состояния.

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

sub = self.$state.sink(receiveValue: { (state) in
 print("state updated: \(state)")
})
афанский
источник