How to implement an NSRunLoop inside an NSOperation How to implement an NSRunLoop inside an NSOperation ios ios

How to implement an NSRunLoop inside an NSOperation


The answer to problem #1

I have an NSOperation which calls an Asynchronous operation in its main method which calls back outside the operation and I need to tell the operation its complete and end the NSOperation:

The following code is amended from above

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

Answer to problem #2 - You cant use

[NSOperationQueue addOperations:.. waitUntilFinished:YES]

Because your main thread will not update, but you also have several OTHER operations which must not execute until this NSOperation is complete, and NONE of them should block the main thread.

Enter...

dispatch_semaphore_t

If you have several dependent NSOperations which you need to launch from the main thread, you can passa dispatch semaphore to the NSOperation, remember that these are Asynchronous calls inside the NSOperation main method, so the NSOperation subclass needs to wait for those callbacks to complete.Also method chaining from callbacks can be a problem.

By passing in a semaphore from the main thread you can use [NSOperation addOperations:... waitUntilFinished: NO] and still prevent other operations from executing until your callbacks have all completed.

Code for the main thread creating the NSOperation

//only one operation will run at a timedispatch_semaphore_t mySemaphore = dispatch_semaphore_create(1);//pass your semaphore into the NSOperation on creationmyOperation = [[YourCustomNSOperation alloc] initWithSemaphore:mySemaphore] autorelease];//call the operation[myOperationQueue addOperations:@[myOperation] waitUntilFinished:NO];

...Code for the 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;}

When your callback method on another thread calls setComplete on the NSOperation3 things will happen,

  1. The runloop will be stopped allowing the NSOperation to complete (which it otherwise would not)

  2. The semaphore will be released allowing other operations sharing the semaphore to run

  3. The NSOperation will complete and be dealloced

If you use method 2 you can wait on arbitrary asynchronous methods invoked from an NSOperationQueue,know that they will complete the runloop, and you can chain callbacks in any way you like, while never blocking the main thread.


I didn't read these answers in any great detail because these approaches are a) way too complicated and b) not using NSOperation the way it's designed to be used. You guys seem to be hacking functionality that already exists.

The solution is to subclass NSOperation and override the getter isConcurrent to return YES. You then implement the - (void)start method and begin your asynchronous task. You are then responsible for finishing it, meaning you have to generate KVO notifications on isFinished and isExecuting so that the NSOperationQueue can know the task is complete.

(UPDATE: Here's how you would subclass NSOperation)(UPDATE 2: Added how you would handle a NSRunLoop if you have code that requires one when working on a background thread. The Dropbox Core API for example)

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


I'm not sure why you would want all the overhead of NSOperation just for a run loop, but I suppose if you are using an operation queue design then maybe it would be useful. The reason I say this is usually you would just perform a selector in background and call CFRunLoopRun from there.

That aside, below is an example NSOperation subclass that uses a run loop. Just subclass it and override willRun and call your method that requires a run loop to work. Once all methods called have finished, thus all run loop sources have been handled - the operation will end automatically. You can test it out by putting a simple perform selector after delay in the willRun method and a break point in completeOperation and you will see the operation will last as long as it takes to finish performing that. Furthermore, if you were to perform after delay something else at that point then the operation will continue to run. As I said it keeps running as long as there is something that requires a run loop to function, even if those are added after it was started.

There is no need for a stop method because as soon as everything has finished and there are no more sources to process it will end automatically.

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

What the heck, here is an example subclass too :-)

@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

Just test it out like this:

TestLoop* t = [[TestLoop alloc] init];[t start];