Reciba una notificación cuando NSOperationQueue finalice todas las tareas

92

NSOperationQueuetiene waitUntilAllOperationsAreFinished, pero no quiero esperar sincrónicamente. Solo quiero ocultar el indicador de progreso en la interfaz de usuario cuando finaliza la cola.

¿Cuál es la mejor manera de lograrlo?

No puedo enviar notificaciones desde mi correo electrónico NSOperationporque no sé cuál será el último y es [queue operations]posible que aún no esté vacío (o peor aún, repoblado) cuando se reciba la notificación.

Kornel
fuente
Compruebe esto si está utilizando GCD en swift 3. stackoverflow.com/a/44562935/1522584
Abhijith

Respuestas:

166

Use KVO para observar la operationspropiedad de su cola, luego puede saber si su cola se ha completado al verificar [queue.operations count] == 0.

En algún lugar del archivo en el que está haciendo el KVO, declare un contexto para KVO como este ( más información ):

static NSString *kQueueOperationsChanged = @"kQueueOperationsChanged";

Cuando configure su cola, haga esto:

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

Entonces haz esto en tu 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];
    }
}

(Esto es asumiendo que NSOperationQueueestá en una propiedad nombrada queue)

En algún momento antes de que su objeto se cancele por completo (o cuando deje de preocuparse por el estado de la cola), deberá cancelar el registro de KVO de esta manera:

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


Anexo: iOS 4.0 tiene una NSOperationQueue.operationCountpropiedad que, según los documentos, cumple con KVO. Sin embargo, esta respuesta seguirá funcionando en iOS 4.0, por lo que sigue siendo útil para la compatibilidad con versiones anteriores.

Nick Forge
fuente
26
Yo diría que debería usar el descriptor de acceso de propiedad, ya que proporciona encapsulación preparada para el futuro (si decide, por ejemplo, inicializar la cola de forma perezosa). El acceso directo a una propiedad por su ivar podría considerarse una optimización prematura, pero realmente depende del contexto exacto. El tiempo que se ahorra al acceder directamente a una propiedad a través de su ivar generalmente será insignificante, a menos que haga referencia a esa propiedad más de 100-1000 veces por segundo (como una estimación increíblemente cruda).
Nick Forge
2
Tentado a votar en contra debido al mal uso de KVO. El uso adecuado se describe aquí: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Nikolai Ruhe
19
@NikolaiRuhe Tienes razón: usar este código al crear una subclase de una clase que usa KVO para observar operationCounten el mismo NSOperationQueueobjeto podría generar errores, en cuyo caso necesitarías usar el argumento de contexto correctamente. Es poco probable que ocurra, pero definitivamente es posible. (Deletrear el problema real es más útil que agregar snark + un enlace)
Nick Forge
6
Encontré una idea interesante aquí . Lo usé para subclasificar NSOperationQueue, agregué una propiedad NSOperation, 'finalOpearation', que se establece como dependiente de cada operación agregada a la cola. Obviamente tuvo que anular addOperation: para hacerlo. También se agregó un protocolo que envía un mensaje a un delegado cuando finalOperation se completa. Ha estado funcionando hasta ahora.
pnizzle
1
¡Mucho mejor! Estaré más feliz cuando se especifiquen las opciones, y la llamada removeObserver: esté envuelta por un @ try / @ catch - No es ideal, pero los documentos de Apple especifican que no hay seguridad al llamar a removeObserver: ... if el objeto no tiene un registro de observador, la aplicación se bloqueará.
Austin
20

Si espera (o desea) algo que coincida con este comportamiento:

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>

Debe tener en cuenta que si se agregan varias operaciones "cortas" a una cola, es posible que vea este comportamiento en su lugar (porque las operaciones se inician como parte de su adición a la cola):

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>

En mi proyecto, necesitaba saber cuándo se completó la última operación, después de que se hubiera agregado una gran cantidad de operaciones a una NSOperationQueue en serie (es decir, maxConcurrentOperationCount = 1) y solo cuando se habían completado todas.

Buscando en Google encontré esta declaración de un desarrollador de Apple en respuesta a la pregunta "¿es una serie FIFO NSoperationQueue?" -

Si todas las operaciones tienen la misma prioridad (que no se cambia después de que la operación se agrega a una cola) y todas las operaciones están siempre - isReady == YES para cuando se colocan en la cola de operaciones, entonces una NSOperationQueue serial es FIFO.

Chris Kane Cocoa Frameworks, Apple

En mi caso es posible saber cuándo se agregó la última operación a la cola. Entonces, después de agregar la última operación, agrego otra operación a la cola, de menor prioridad, que no hace más que enviar la notificación de que la cola se ha vaciado. Dada la declaración de Apple, esto garantiza que solo se envíe un aviso único después de que se hayan completado todas las operaciones.

Si las operaciones se están agregando de una manera que no permite detectar la última (es decir, no determinista), entonces creo que debe seguir los enfoques de KVO mencionados anteriormente, con una lógica de protección adicional agregada para tratar de detectar si más se pueden agregar operaciones.

:)

software evolucionado
fuente
Hola, ¿sabe si es posible recibir una notificación cuando finaliza cada operación en la cola mediante el uso de una NSOperationQueue con maxConcurrentOperationCount = 1?
Sefran2
@fran: Haría que las operaciones publicaran una notificación al finalizar. De esa manera, otros módulos pueden registrarse como observadores y responder a medida que se completa. Si su @selector toma un objeto de notificación, puede recuperar fácilmente el objeto que publicó la notificación, en caso de que necesite más detalles sobre qué operación acaba de completar.
software evolucionó el
17

¿Qué tal si agrega una NSOperation que depende de todas las demás para que se ejecute al final?

Mayormente sí
fuente
1
Puede que funcione, pero es una solución de gran peso y sería complicado de administrar si necesita agregar nuevas tareas a la cola.
Kornel
este es realmente muy elegante y el que más prefiero! tu mi voto.
Yariv Nissim
1
Personalmente, esta es mi solución favorita. Puede crear fácilmente una NSBlockOperation simple para el bloque de finalización que depende de todas las demás operaciones.
Puneet Sethi
Es posible que tenga un problema de que no se llama a NSBlockOperation cuando se cancela la cola. Por lo tanto, debe realizar su propia operación que crea un error cuando se cancela y llama a un bloque con un parámetro de error.
Malhal
¡Esta es la mejor respuesta!
trampero
12

Una alternativa es utilizar GCD. Refiérase a esto como referencia.

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

Así es como lo hago.

Configure la cola y regístrese para cambios en la propiedad de operaciones:

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

... y el observador (en este 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;
}

En este ejemplo, "ruleta" es una UIActivityIndicatorViewmuestra de que algo está sucediendo. Obviamente puedes cambiar para adaptarse ...

Kris Jenkins
fuente
2
Ese forbucle parece potencialmente costoso (¿y si cancela todas las operaciones a la vez? ¿No obtendría un rendimiento cuadrático cuando se limpia la cola?)
Kornel
Buena, pero tenga cuidado con los hilos, porque, de acuerdo con la documentación: "... Las notificaciones KVO asociadas con una cola de operaciones pueden ocurrir en cualquier hilo". Probablemente, necesitaría mover el flujo de ejecución a la cola de operaciones principal antes de actualizar la ruleta
Igor Vasilev
3

Estoy usando una categoría para hacer esto.

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

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

brandonscript
fuente
¿Por qué es esto una finalización ? Una NSOperationQueue no se completa, simplemente se vacía. El estado vacío se puede ingresar varias veces durante la vida útil de una NSOperationQueue.
CouchDeveloper
Esto no funciona si op1 y op2 terminan antes de que se llame a setCompletion.
malhal
Excelente respuesta, solo una advertencia de que se llama al bloque de finalización cuando la cola termina con el inicio de toda la operación. Iniciando operaciones! = Las operaciones están completas.
Saqib Saud
Hmm, respuesta antigua, pero apuesto a que waitUntilFinisheddebería serYES
brandonscript
3

A partir de IOS 13.0 , la operationCount y operación propiedades están en desuso. Es igual de sencillo realizar un seguimiento de la cantidad de operaciones en su cola y enviar una Notificación cuando todas se hayan completado. Este ejemplo también funciona con una subclase asincrónica 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
    }
}

A continuación se muestra una subclase de operación para operaciones asincrónicas sencillas

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
fuente
¿Dónde se decrementOperationCount()invoca el método?
iksnae
@iksnae - Actualicé mi respuesta con una subcasa de Operation . Yo uso decrementOperationCount () dentro del didSet de mi variable de estado . ¡Espero que esto ayude!
Caleb Lindsey
2

¿Qué pasa con el uso de KVO para observar la operationCountpropiedad de la cola? Entonces te enterarías cuando la cola se vació y también cuando dejó de estar vacía. Tratar con el indicador de progreso puede ser tan simple como hacer algo como:

[indicator setHidden:([queue operationCount]==0)]
Sixten Otto
fuente
¿Funcionó esto para usted? En mi aplicación, el NSOperationQueuede 3.1 se queja de que no es compatible con KVO para la clave operationCount.
zoul
En realidad, no probé esta solución en una aplicación, no. No puedo decir si el OP lo hizo. Pero la documentación establece claramente que debería funcionar. Presentaría un informe de error. developer.apple.com/iphone/library/documentation/Cocoa/…
Sixten Otto
No hay propiedad operationCount en NSOperationQueue en el SDK de iPhone (al menos no a partir de 3.1.3). Debes haber estado mirando la página de documentación de Max OS X ( developer.apple.com/Mac/library/documentation/Cocoa/Reference/… )
Nick Forge
1
El tiempo cura todas las heridas ... y, a veces, las respuestas incorrectas. A partir de iOS 4, la operationCountpropiedad está presente.
Sixten Otto
2

Agregue la última operación como:

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

Entonces:

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

}
pvllnspk
fuente
3
cuando las tareas se ejecutan al mismo tiempo, es un enfoque incorrecto.
Marcin
2
Y cuando se cancela la cola ni siquiera se inicia esta última operación.
malhal
2

Con ReactiveObjC encuentro que esto funciona muy bien:

// 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!");
    }
}];
Persona maravillosa
fuente
1

Para su información, puede lograr esto con GCD dispatch_group en swift 3 . Puede recibir una notificación cuando finalicen todas las tareas.

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
fuente
¿Cuál es la versión mínima de iOS para usar esto?
Nitesh Borad
Está disponible desde swift 3, iOS 8 o superior.
Abhijith
0

Puede crear uno nuevo NSThreado ejecutar un selector en segundo plano y esperar allí. Cuando elNSOperationQueue termine, puede enviar una notificación propia.

Estoy pensando en 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
fuente
Parece un poco tonto crear hilo solo para ponerlo a dormir.
Kornel
Estoy de acuerdo. Aún así, no pude encontrar otra forma de evitarlo.
pgb
¿Cómo se aseguraría de que solo haya un hilo esperando? Pensé en la bandera, pero eso debe protegerse contra las condiciones de la carrera, y terminé usando demasiado NSLock para mi gusto.
Kornel
Creo que puede envolver el NSOperationQueue en algún otro objeto. Siempre que pone en cola una operación NSO, incrementa un número y lanza un hilo. Cada vez que termina un hilo, disminuye ese número en uno. Estaba pensando en un escenario en el que podría poner todo en cola de antemano y luego iniciar la cola, por lo que solo necesitaría un hilo en espera.
pgb
0

Si usa esta Operación como su clase base, podría pasar el whenEmpty {}bloque a OperationQueue :

let queue = OOperationQueue()
queue.addOperation(op)
queue.addOperation(delayOp)

queue.addExecution { finished in
    delay(0.5) { finished() }
}

queue.whenEmpty = {
    print("all operations finished")
}
usuario1244109
fuente
1
El valor de tipo 'OperationQueue' no tiene miembro 'whenEmpty'
Dale
@Dale, si haces clic en el enlace, te llevará a una página de github donde se explica todo. Si mal no recuerdo, la respuesta se escribió cuando OperationQueue de la Fundación todavía se llamaba NSOperationQueue; así que quizás hubo menos ambigüedad.
user1244109
Mi mal ... Llegué a la falsa conclusión de que la "OperationQueue" anterior era la "OperationQueue" de Swift 4.
Dale
0

Sin 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
fuente
0

Si llegaste aquí buscando una solución con combine, terminé escuchando mi propio objeto de estado.

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

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