objective c - Cómo implementar un NSRunLoop dentro de una NSOperation
objective-c ios (3)
Estoy publicando esta pregunta porque he visto mucha confusión sobre este tema y como resultado pasé varias horas depurando subclases NSOperation.
El problema es que NSOperation no le ayuda mucho cuando ejecuta métodos asíncronos que no se completan hasta que finaliza la devolución de llamada asíncrona.
Si la NSOperation en sí misma es el delegado de devolución de llamada, puede que ni siquiera sea suficiente para completar correctamente la operación debido a que la devolución de llamada se produce en un subproceso diferente.
Digamos que está en el hilo principal y crea una NSOperation y la agrega a una NSOperationQueue. El código dentro de la NSOperation dispara una llamada asíncrona que vuelve a llamar a algún método en AppDelegate o un controlador de vista.
No puedes bloquear el hilo principal o la IU se bloqueará, así que tienes dos opciones.
1) Cree una NSOperation y agréguela a NSOperationQueue con la siguiente firma:
[NSOperationQueue addOperations: @ [myOp] waitUntilFinished :?]
Buena suerte con eso. Las operaciones asíncronas generalmente requieren un runloop, por lo que no funcionarán a menos que subclasifique NSOperation o use un bloque, pero incluso un bloque no funcionará si tiene que "completar" la NSOperation indicándole cuándo finalizó la devolución de llamada.
Entonces ... subclase NSOperation con algo similar a lo siguiente para que la devolución de llamada pueda indicar la operación cuando haya finalizado:
//you create an NSOperation subclass it includes a main method that
//keeps the runloop going as follows
//your NSOperation subclass has a BOOL field called "complete"
-(void) main
{
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//I do some stuff which has async callbacks to the appDelegate or any other class (very common)
while (!complete && [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
}
//I also have a setter that the callback method can call on this operation to
//tell the operation that its done,
//so it completes, ends the runLoop and ends the operation
-(void) setComplete {
complete = true;
}
//I override isFinished so that observers can see when Im done
// - since my "complete" field is local to my instance
-(BOOL) isFinished
{
return complete;
}
Bien, esto no funciona, ¡lo hemos eliminado del camino!
2) El segundo problema con este método es que digamos que lo anterior realmente funcionó (que no lo hace) en los casos en que los runLoops tienen que terminar correctamente, (o terminar realmente en absoluto desde una llamada de método externo en una devolución de llamada)
Asumamos un segundo Im en el subproceso principal cuando llamo a esto, a menos que quiera que la interfaz de usuario se bloquee un poco y no pinte nada, no puedo decir "waitUntilFinished: YES" en el método NOperationQueue addOperation ...
Entonces, ¿cómo puedo lograr el mismo comportamiento que waitUntilFinished: YES sin bloquear el hilo principal?
Ya que hay tantas preguntas sobre el comportamiento de RunLoops, NSOperationQueues y Asynch en Cocoa, publicaré mi solución como respuesta a esta pregunta.
Tenga en cuenta que solo estoy respondiendo a mi propia pregunta porque verifiqué meta.stackoverflow y me dijeron que esto es aceptable y alentador. Espero que la respuesta que sigue ayude a las personas a comprender por qué sus runloops se están bloqueando en las operaciones NSO y cómo pueden completar correctamente las operaciones NSO desde devoluciones de llamada (Callbacks en otros hilos)
No estoy seguro de por qué querría toda la sobrecarga de NSOperation solo para un ciclo de ejecución, pero supongo que si está utilizando un diseño de cola de operaciones, tal vez sea útil. La razón por la que digo que esto es por lo general debería realizar un selector en segundo plano y llamar a CFRunLoopRun desde allí.
Aparte de eso, a continuación hay un ejemplo de subclase NSOperation que usa un bucle de ejecución. Simplemente haga una subclase y anule willRun y llame a su método que requiere un ciclo de ejecución para funcionar. Una vez que todos los métodos llamados hayan finalizado, se manejarán todas las fuentes de bucle de ejecución, la operación finalizará automáticamente. Puede probarlo colocando un simple selector de ejecución después de la demora en el método willRun y un punto de interrupción en completeOperation y verá que la operación durará todo el tiempo que sea necesario para terminar de realizarla. Además, si tuviera que realizar después de la demora algo más en ese punto, la operación continuará ejecutándose. Como dije, se sigue ejecutando siempre que haya algo que requiera un bucle de ejecución para funcionar, incluso si se agregan después de que se inició.
No hay necesidad de un método de detención porque tan pronto como todo haya terminado y no haya más fuentes para procesarlo, finalizará automáticamente.
MHRunLoopOperation.h
#import <Foundation/Foundation.h>
@interface MHRunLoopOperation : NSOperation
// Override and call methods that require a run loop.
// No need to call super because the default implementation does nothing.
-(void)willRun;
@end
MHRunLoopOperation.m
#import "MHRunLoopOperation.h"
@interface MHRunLoopOperation()
@property (nonatomic, assign) BOOL isExecuting;
@property (nonatomic, assign) BOOL isFinished;
@end
@implementation MHRunLoopOperation
- (BOOL)isAsynchronous {
return YES;
}
- (void)start {
// Always check for cancellation before launching the task.
if (self.isCancelled)
{
// Must move the operation to the finished state if it is canceled.
self.isFinished = YES;
return;
}
// If the operation is not canceled, begin executing the task.
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
_isExecuting = YES;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)main {
@try {
// Do the main work of the operation here.
[self willRun];
CFRunLoopRun(); // It waits here until all method calls or remote data requests that required a run loop have finished. And after that then it continues.
[self completeOperation];
}
@catch(...) {
// Do not rethrow exceptions.
}
}
-(void)willRun{
// To be overridden by a subclass and this is where calls that require a run loop are done, e.g. remote data requests are started.
}
-(void)completeOperation{
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
_isExecuting = NO;
_isFinished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
@end
Qué diablos, aquí también hay una subclase de ejemplo :-)
@interface TestLoop : MHRunLoopOperation
@end
@implementation TestLoop
// override
-(void)willRun{
[self performSelector:@selector(test) withObject:nil afterDelay:2];
}
-(void)test{
NSLog(@"test");
// uncomment below to make keep it running forever
//[self performSelector:@selector(test) withObject:nil afterDelay:2];
}
// overridden just for demonstration purposes
-(void)completeOperation{
NSLog(@"completeOperation");
[super completeOperation];
}
@end
Solo pruébalo así:
TestLoop* t = [[TestLoop alloc] init];
[t start];
No leí estas respuestas con gran detalle porque estos enfoques son a) demasiado complicados yb) no utilizan la NSOperation de la forma en que están diseñados para ser utilizados. Ustedes parecen estar hackeando la funcionalidad que ya existe.
La solución es la subclase NSOperation y anular el getter isConcurrent para devolver SÍ. Luego, implementa el método de inicio - (vacío) y comienza tu tarea asíncrona. Entonces, usted es responsable de finalizarlo, lo que significa que debe generar notificaciones KVO en isFinished e isExecuting para que NSOperationQueue pueda saber que la tarea está completa.
(ACTUALIZACIÓN: así es como se subclasificaría NSOperation) (ACTUALIZACIÓN 2: Se agregó cómo se manejaría un NSRunLoop si tiene un código que requiere uno cuando se trabaja en un subproceso en segundo plano. La API de Dropbox Core, por ejemplo)
// HSConcurrentOperation : NSOperation
#import "HSConcurrentOperation.h"
@interface HSConcurrentOperation()
{
@protected
BOOL _isExecuting;
BOOL _isFinished;
// if you need run loops (e.g. for libraries with delegate callbacks that require a run loop)
BOOL _requiresRunLoop;
NSTimer *_keepAliveTimer; // a NSRunLoop needs a source input or timer for its run method to do anything.
BOOL _stopRunLoop;
}
@end
@implementation HSConcurrentOperation
- (instancetype)init
{
self = [super init];
if (self) {
_isExecuting = NO;
_isFinished = NO;
}
return self;
}
- (BOOL)isConcurrent
{
return YES;
}
- (BOOL)isExecuting
{
return _isExecuting;
}
- (BOOL)isFinished
{
return _isFinished;
}
- (void)start
{
[self willChangeValueForKey:@"isExecuting"];
NSLog(@"BEGINNING: %@", self.description);
_isExecuting = YES;
[self didChangeValueForKey:@"isExecuting"];
_requiresRunLoop = YES; // depends on your situation.
if(_requiresRunLoop)
{
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// run loops don''t run if they don''t have input sources or timers on them. So we add a timer that we never intend to fire and remove him later.
_keepAliveTimer = [NSTimer timerWithTimeInterval:CGFLOAT_MAX target:self selector:@selector(timeout:) userInfo:nil repeats:nil];
[runLoop addTimer:_keepAliveTimer forMode:NSDefaultRunLoopMode];
[self doWork];
NSTimeInterval updateInterval = 0.1f;
NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:updateInterval];
while (!_stopRunLoop && [runLoop runMode: NSDefaultRunLoopMode beforeDate:loopUntil])
{
loopUntil = [NSDate dateWithTimeIntervalSinceNow:updateInterval];
}
}
else
{
[self doWork];
}
}
- (void)timeout:(NSTimer*)timer
{
// this method should never get called.
[self finishDoingWork];
}
- (void)doWork
{
// do whatever stuff you need to do on a background thread.
// Make network calls, asynchronous stuff, call other methods, etc.
// and whenever the work is done, success or fail, whatever
// be sure to call finishDoingWork.
[self finishDoingWork];
}
- (void)finishDoingWork
{
if(_requiresRunLoop)
{
// this removes (presumably still the only) timer from the NSRunLoop
[_keepAliveTimer invalidate];
_keepAliveTimer = nil;
// and this will kill the while loop in the start method
_stopRunLoop = YES;
}
[self finish];
}
- (void)finish
{
// generate the KVO necessary for the queue to remove him
[self willChangeValueForKey:@"isExecuting"];
[self willChangeValueForKey:@"isFinished"];
_isExecuting = NO;
_isFinished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
@end
La respuesta al problema # 1
Tengo una operación NSO que llama a una operación asíncrona en su método principal que llama fuera de la operación y necesito decirle a la operación que está completa y terminar la operación NSO:
El siguiente código se modifica desde arriba
//you create an NSOperation subclass it includes a main method that
//keeps the runloop going as follows
//your NSOperation subclass has a BOOL field called "complete"
//ADDED: your NSOperation subclass has a BOOL field called "stopRunLoop"
//ADDED: your NSOperation subclass has a NSThread * field called "myThread"
-(void) main
{
myThread = [NSThread currentThread];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//I do some stuff which has async callbacks to the appDelegate or any other class (very common)
while (!stopRunLoop && [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
//in an NSOperation another thread cannot set complete
//even with a method call to the operation
//this is needed or the thread that actually invoked main and
//KVO observation will not see the value change
//Also you may need to do post processing before setting complete.
//if you just set complete on the thread anything after the
//runloop will not be executed.
//make sure you are actually done.
complete = YES;
}
-(void) internalComplete
{
stopRunloop = YES;
}
//This is needed to stop the runLoop,
//just setting the value from another thread will not work,
//since the thread that created the NSOperation subclass
//copied the member fields to the
//stack of the thread that ran the main() method.
-(void) setComplete {
[self performSelector:@selector(internalComplete) onThread:myThread withObject:nil waitUntilDone:NO];
}
//override isFinished same as before
-(BOOL) isFinished
{
return complete;
}
Respuesta al problema # 2 - No puedes usar
[NSOperationQueue addOperations:.. waitUntilFinished:YES]
Debido a que su subproceso principal no se actualizará, pero también tiene varias OTRAS operaciones que no deben ejecutarse hasta que esta NSOperation se complete, y NINGUNA de ellas debe bloquear el subproceso principal.
Entrar...
dispatch_semaphore_t
Si tiene varias operaciones NSO dependientes que necesita iniciar desde el hilo principal, puede pasar un semáforo de envío a la operación NSO, recuerde que estas son llamadas asíncronas dentro del método principal de operación NSO , por lo que la subclase NSOperation debe esperar a que se completen esas devoluciones de llamada . También el encadenamiento de métodos a partir de devoluciones de llamada puede ser un problema.
Al pasar un semáforo desde el hilo principal, puede usar [NSOperation addOperations: ... waitUntilFinished: NO] y aun así evitar que se ejecuten otras operaciones hasta que todas las devoluciones de llamada se hayan completado.
Código para el hilo principal creando la NSOperation.
//only one operation will run at a time
dispatch_semaphore_t mySemaphore = dispatch_semaphore_create(1);
//pass your semaphore into the NSOperation on creation
myOperation = [[YourCustomNSOperation alloc] initWithSemaphore:mySemaphore] autorelease];
//call the operation
[myOperationQueue addOperations:@[myOperation] waitUntilFinished:NO];
... Código para la NSOperation
//In the main method of your Custom NSOperation - (As shown above) add this call before
//your method does anything
//my custom NSOperation subclass has a field of type dispatch_semaphore_t
//named "mySemaphore"
-(void) main
{
myThread = [NSThread currentThread];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//grab the semaphore or wait until its available
dispatch_semaphore_wait(mySemaphore, DISPATCH_TIME_FOREVER);
//I do some stuff which has async callbacks to the appDelegate or any other class (very common)
while (!stopRunLoop && [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
//release the semaphore
dispatch_semaphore_signal(mySemaphore);
complete = YES;
}
Cuando su método de devolución de llamada en otro subproceso llama a setComplete en el NSOperation 3, sucederán cosas,
El runloop se detendrá y permitirá que la NSOperation se complete (lo que de otro modo no lo haría)
El semáforo se lanzará permitiendo que se ejecuten otras operaciones que comparten el semáforo
La NSOperation se completará y será desasignada
Si usa el método 2, puede esperar métodos asíncronos arbitrarios invocados desde una NSOperationQueue, sepa que completarán el runloop, y puede encadenar devoluciones de llamada de la forma que desee, sin bloquear el hilo principal.