Receba uma notificação quando NSOperationQueue terminar todas as tarefas

92

NSOperationQueuetem waitUntilAllOperationsAreFinished, mas não quero esperar sincronizadamente por isso. Eu só quero ocultar o indicador de progresso na interface do usuário quando a fila terminar.

Qual é a melhor maneira de fazer isso?

Não posso enviar notificações do meu NSOperations, porque não sei qual vai ser a última, e [queue operations]posso não estar vazio ainda (ou pior - preenchido novamente) quando a notificação for recebida.

Kornel
fonte
Verifique se você está usando GCD em swift 3. stackoverflow.com/a/44562935/1522584
Abhijith

Respostas:

166

Use KVO para observar a operationspropriedade de sua fila, então você pode saber se sua fila foi concluída verificando [queue.operations count] == 0.

Em algum lugar do arquivo em que você está fazendo o KVO, declare um contexto para o KVO como este ( mais informações ):

static NSString *kQueueOperationsChanged = @"kQueueOperationsChanged";

Ao configurar sua fila, faça o seguinte:

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

Então faça isso no seu 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];
    }
}

(Isso pressupõe que você NSOperationQueueestá em uma propriedade chamada queue)

Em algum ponto antes de seu objeto ser totalmente desalocado (ou quando ele parar de se preocupar com o estado da fila), você precisará cancelar o registro do KVO desta forma:

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


Adendo: iOS 4.0 possui uma NSOperationQueue.operationCountpropriedade que, de acordo com os documentos, é compatível com KVO. Essa resposta ainda funcionará no iOS 4.0, portanto, ainda é útil para compatibilidade com versões anteriores.

Nick Forge
fonte
26
Eu diria que você deve usar o acessador de propriedade, uma vez que fornece encapsulamento à prova de futuro (se você decidir, por exemplo, inicializar lentamente a fila). Acessar diretamente uma propriedade por meio de seu ivar pode ser considerado uma otimização prematura, mas na verdade depende do contexto exato. O tempo economizado ao acessar diretamente uma propriedade por meio de seu ivar geralmente será insignificante, a menos que você esteja fazendo referência a essa propriedade mais de 100-1000 vezes por segundo (como uma estimativa incrivelmente rudimentar).
Nick Forge
2
Tentação de downvote devido ao uso incorreto de KVO. Uso adequado descrito aqui: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Nikolai Ruhe
19
@NikolaiRuhe Você está correto - usar este código ao criar uma subclasse de uma classe que usa KVO para observar operationCountno mesmo NSOperationQueueobjeto poderia levar a bugs, caso em que você precisaria usar o argumento de contexto corretamente. É improvável que ocorra, mas definitivamente possível. (Soletrar o problema real é mais útil do que adicionar snark + um link)
Nick Forge
6
Encontrei uma ideia interessante aqui . Usei isso para criar a subclasse de NSOperationQueue, adicionando uma propriedade NSOperation, 'finalOpearation', que é definida como dependente de cada operação adicionada à fila. Obviamente, foi necessário substituir addOperation: para fazer isso. Também foi adicionado um protocolo que envia uma mensagem para um delegado quando finalOperation é concluído. Tem trabalhado até agora.
pnizzle de
1
Muito melhor! Ficarei muito feliz quando as opções forem especificadas e a chamada removeObserver: for envolvida por um @ try / @ catch - não é o ideal, mas os documentos da apple especificam que não há segurança ao chamar removeObserver: ... if o objeto não tem um registro de observador, o aplicativo irá travar.
Austin,
20

Se você está esperando (ou deseja) algo que corresponda a este comportamento:

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>

Você deve estar ciente de que, se várias operações "curtas" estiverem sendo adicionadas a uma fila, você poderá ver este comportamento (porque as operações são iniciadas como parte da adição à fila):

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>

Em meu projeto, eu precisava saber quando a última operação foi concluída, depois que um grande número de operações foi adicionado a um NSOperationQueue serial (ou seja, maxConcurrentOperationCount = 1) e somente quando todas foram concluídas.

Pesquisando no Google, encontrei esta declaração de um desenvolvedor da Apple em resposta à pergunta "is a serial NSoperationQueue FIFO?" -

Se todas as operações têm a mesma prioridade (que não é alterada depois que a operação é adicionada a uma fila) e todas as operações são sempre - isReady == YES no momento em que são colocadas na fila de operações, então um NSOperationQueue serial é FIFO.

Chris Kane Cocoa Frameworks, Apple

No meu caso é possível saber quando a última operação foi adicionada à fila. Então, depois que a última operação é adicionada, eu adiciono outra operação à fila, de menor prioridade, que não faz nada além de enviar a notificação de que a fila foi esvaziada. De acordo com a declaração da Apple, isso garante que apenas um único aviso seja enviado somente após a conclusão de todas as operações.

Se as operações estão sendo adicionadas de uma maneira que não permite detectar a última (ou seja, não determinística), então eu acho que você deve ir com as abordagens KVO mencionadas acima, com lógica de guarda adicional adicionada para tentar detectar se mais operações podem ser adicionadas.

:)

software evoluiu
fonte
Oi, você sabe se e como é possível ser notificado quando cada operação na fila termina usando um NSOperationQueue com maxConcurrentOperationCount = 1?
Sefran2
@fran: Eu gostaria que as operações publicassem uma notificação após a conclusão. Dessa forma, outros módulos podem se registrar como observadores e responder conforme cada um é concluído. Se o seu @selector pegar um objeto de notificação, você pode facilmente recuperar o objeto que postou a notificação, caso precise de mais detalhes sobre o op que acabou de concluir.
software evoluiu em
17

Que tal adicionar uma operação NSO que é dependente de todas as outras para que seja executada por último?

Principalmente Sim
fonte
1
Pode funcionar, mas é uma solução pesada e seria difícil de gerenciar se você precisar adicionar novas tarefas à fila.
Kornel
este é realmente muito elegante e o que eu mais preferi! você meu voto.
Yariv Nissim
1
Pessoalmente, essa é minha solução favorita. Você pode criar facilmente um NSBlockOperation simples para o bloco de conclusão que depende de todas as outras operações.
Puneet Sethi
Você pode ter um problema de que NSBlockOperation não é chamado quando a fila é cancelada. Portanto, você precisa fazer sua própria operação que cria um erro quando cancelada e chama um bloco com um parâmetro de erro.
malhal
Esta é a melhor resposta!
trapper
12

Uma alternativa é usar o GCD. Consulte isso como referência.

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

É assim que eu faço.

Configure a fila e registre-se para alterações na propriedade de operações:

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

... e o observador (neste caso self) implementa:

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

Neste exemplo, "spinner" UIActivityIndicatorViewmostra que algo está acontecendo. Obviamente, você pode mudar para se adequar ...

Kris Jenkins
fonte
2
Esse forloop parece potencialmente caro (e se você cancelar todas as operações de uma vez? Isso não teria um desempenho quadrático quando a fila estivesse sendo limpa?)
Kornel
Boa, mas tenha cuidado com os tópicos, pois, de acordo com a documentação: "... notificações KVO associadas a uma fila de operação podem ocorrer em qualquer tópico." Provavelmente, você precisaria mover o fluxo de execução para a fila de operação principal antes de atualizar o controle giratório
Igor Vasilev
3

Estou usando uma categoria para fazer isso.

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

Uso :

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

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

brandonscript
fonte
Por que isso é uma conclusão ? Uma NSOperationQueue não é concluída - ela simplesmente fica vazia. O estado vazio pode ser inserido várias vezes durante o tempo de vida de um NSOperationQueue.
CouchDeveloper
Isso não funciona se op1 e op2 terminam antes de setCompletion ser chamado.
malhal
Excelente resposta, apenas uma advertência de que o bloco de conclusão é chamado quando a fila termina com o início de toda a operação. Iniciando as operações! = As operações estão concluídas.
Saqib Saud
Hmm resposta antiga, mas aposto que waitUntilFinisheddeveria serYES
brandonscript
3

Como de iOS 13,0 , o operationCount e operação propriedades estão obsoletos. É muito simples controlar o número de operações em sua fila e disparar uma Notificação quando todas elas forem concluídas. Este exemplo também funciona com uma subclasse assíncrona de 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
    }
}

Abaixo está uma subclasse de Operação para operações assíncronas fáceis

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
fonte
onde o decrementOperationCount()método é invocado?
iksnae
@iksnae - Eu atualizei minha resposta com uma subcategoria de Operação . Eu uso decrementOperationCount () dentro do didSet da minha variável de estado . Espero que isto ajude!
Caleb Lindsey
2

Que tal usar KVO para observar a operationCountpropriedade da fila? Então, você ouviria quando a fila esvaziasse e também quando ela parasse de esvaziar. Lidar com o indicador de progresso pode ser tão simples quanto fazer algo como:

[indicator setHidden:([queue operationCount]==0)]
Sixten Otto
fonte
Isso funcionou para você? Na minha aplicação, o NSOperationQueuefrom 3.1 reclama que não é compatível com KVO para a chave operationCount.
zoul
Na verdade, não tentei essa solução em um aplicativo, não. Não posso dizer se o OP sim. Mas a documentação afirma claramente que deve funcionar. Eu faria um relatório de bug. developer.apple.com/iphone/library/documentation/Cocoa/…
Sixten Otto
Não há propriedade operationCount em NSOperationQueue no iPhone SDK (pelo menos não a partir de 3.1.3). Você deve ter consultado a página de documentação do Max OS X ( developer.apple.com/Mac/library/documentation/Cocoa/Reference/… )
Nick Forge
1
O tempo cura todas as feridas ... e às vezes respostas erradas. A partir do iOS 4, a operationCountpropriedade está presente.
Sixten Otto
2

Adicione a última operação como:

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

Assim:

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

}
pvllnspk
fonte
3
quando as tarefas são executadas simultaneamente, é uma abordagem errada.
Marcin
2
E quando a fila é cancelada, esta última operação nem foi iniciada.
malhal
2

Com ReactiveObjC acho que isso funciona muito bem:

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

Para sua informação, você pode conseguir isso com GCD dispatch_group no swift 3 . Você pode ser notificado quando todas as tarefas forem concluídas.

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
fonte
Qual é a versão mínima do iOS para usar isso?
Nitesh Borad
Está disponível a partir do swift 3, iOS 8 ou superior.
Abhijith
0

Você pode criar um novo NSThreadou executar um seletor em segundo plano e aguardar lá. Quando NSOperationQueueterminar, você mesmo pode enviar uma notificação.

Estou pensando em algo como:

- (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
fonte
Parece um pouco bobo criar thread apenas para colocá-lo para dormir.
Kornel
Concordo. Ainda assim, não consegui encontrar outra maneira de contornar isso.
pgb
Como você garantiria que apenas um segmento está esperando? Pensei em flag, mas ela precisa ser protegida das condições de corrida, e acabei usando muito NSLock para o meu gosto.
Kornel
Acho que você pode envolver o NSOperationQueue em algum outro objeto. Sempre que você enfileira uma operação NSO, você incrementa um número e inicia um thread. Sempre que um thread termina, você diminui esse número em um. Eu estava pensando em um cenário em que você poderia enfileirar tudo de antemão e, em seguida, iniciar a fila, de modo que você só precisaria de um thread de espera.
pgb
0

Se você usar esta Operação como sua classe base, poderá passar o whenEmpty {}bloco para 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
fonte
O valor do tipo 'OperationQueue' não tem membro 'whenEmpty'
Dale
@Dale se você clicar no link, irá levá-lo a uma página do github onde tudo é explicado. Se bem me lembro, a resposta foi escrita quando a OperationQueue da Fundação ainda se chamava NSOperationQueue; então talvez houvesse menos ambiguidade.
user1244109
Meu mal ... Eu fiz a falsa conclusão de que o "OperationQueue" acima era "OperationQueue" do Swift 4.
Dale
0

Sem 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
fonte
0

Se você chegou aqui procurando uma solução com combine - acabei ouvindo apenas meu próprio objeto de estado.

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

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