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

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

Какой лучший способ выполнить это?

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

Ответ 1

Используйте 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, поэтому он по-прежнему полезен для обратной совместимости.

Ответ 2

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

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) и только тогда, когда все они были завершены.

Googling. Я нашел это выражение от разработчика Apple в ответ на вопрос: "Является ли последовательный NSOPAQueue FIFO?" -

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

Крис Кейн Cocoa Рамки, Apple

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

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

:)

Ответ 3

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

Ответ 4

Один из вариантов - использовать GCD. Обратитесь к this в качестве ссылки.

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

В этом примере "spinner" - это UIActivityIndicatorView, показывающий, что что-то происходит. Очевидно, вы можете изменить в соответствии с...

Ответ 6

С ReactiveCocoa Я считаю, что это работает красиво:

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

Ответ 7

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

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

Ответ 8

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

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]; 

}

Ответ 9

FYI, вы можете добиться этого с помощью GCD dispatch_group в быстро <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")
}

Ответ 10

Вы можете создать новый 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"];
}

Ответ 11

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

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 completion here (launched in main thread!)
}];

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

Ответ 12

Если вы используете этот Operation в качестве базового класса, вы можете передать блок 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")
}