如何在NSOperation中实现NSRunLoop

del*_*ser 18 objective-c objective-c-runtime ios objective-c-blocks

我发布这个问题,因为我看到很多关于这个主题的混淆,我花了几个小时调试NSOperation子类作为结果.

问题是,当您执行异步方法时,NSOperation对您没有多大帮助,这些异步方法在异步回调完成之前实际上并不完整.

如果NSOperation本身是回调委托,由于在不同的线程上发生回调,它甚至可能不足以正确完成操作.

假设您在主线程中并创建了一个NSOperation并将其添加到NSOperationQueue,NSOperation中的代码会触发异步调用,该调用将调用AppDelegate或视图控制器上的某些方法.

您无法阻止主线程或UI将锁定,因此您有两个选项.

1)创建一个NSOperation并使用以下签名将其添加到NSOperationQueue:

[NSOperationQueue addOperations:@ [myOp] waitUntilFinished:?]

祝你好运.异步操作通常需要一个runloop,因此它不会工作,除非你继承NSOperation或使用一个块,但是如果你必须通过在回调完成时告诉它来"完成"NSOperation,那么即使一个块也不会工作.

所以......你用类似于下面的东西对NSOperation进行子类化,这样回调可以告诉操作何时完成:

//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;
}
Run Code Online (Sandbox Code Playgroud)

好的 - 这绝对不起作用 - 我们已经解决了!

2)这个方法的第二个问题是,假设runLoops必须正确终止(或者实际上从回调中的外部方法调用终止),上面的实际工作(它没有)

让我假设在我调用它时主线程中的第二个Im,除非我想让UI锁定一点,而不是绘制任何东西,我不能在NSOperationQueue addOperation方法上说"waitUntilFinished:YES"...

那么如何实现与waitUntilFinished相同的行为:YES而不锁定主线程?

由于在Cocoa中有关于runLoops,NSOperationQueues和Asynch行为的问题很多,我将发布我的解决方案作为这个问题的答案.

请注意,我只回答了我自己的问题,因为我检查了meta.stackoverflow并且他们说这是可以接受和鼓励的,我希望下面的答案可以帮助人们理解为什么他们的runloops锁定在NSOperations以及他们如何从外部正确完成NSOperations回调.(其他线程的回调)

del*_*ser 15

问题#1的答案

我有一个NSOperation在其main方法中调用异步操作,该方法在操作外调用,我需要告诉操作完成并结束NSOperation:

以下代码从上面修改

//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;
}
Run Code Online (Sandbox Code Playgroud)

对问题#2的回答 - 你无法使用

[NSOperationQueue addOperations:.. waitUntilFinished:YES]
Run Code Online (Sandbox Code Playgroud)

因为你的主线程不会更新,但你也有几个OTHER操作,在NSOperation完成之前一定不能执行,而且它们中的NONE应该阻塞主线程.

输入...

dispatch_semaphore_t
Run Code Online (Sandbox Code Playgroud)

如果你需要从主线程启动几个依赖的NSOperations,你可以将一个调度信号量传递给NSOperation,记住这些是NSOperation主方法中的异步调用,所以NSOperation子类需要等待那些回调完成.从回调链接的方法也可能是个问题.

通过从主线程传入信号量,您可以使用[NSOperation addOperations:... waitUntilFinished:NO]并仍然阻止其他操作执行,直到您的回调全部完成.

创建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];
Run Code Online (Sandbox Code Playgroud)

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

}
Run Code Online (Sandbox Code Playgroud)

当你的另一个线程上的回调方法在NSOperation 3上调用setComplete时会发生一些事情,

  1. runloop将被停止,允许NSOperation完成(否则不会)

  2. 将释放信号量,允许共享信号量的其他操作运行

  3. NSOperation将完成并被解除分配

如果你使用方法2,你可以等待从NSOperationQueue调用的任意异步方法,知道它们将完成runloop,你可以以任何你喜欢的方式链接回调,而不会阻塞主线程.


hor*_*oe7 6

我没有详细阅读这些答案,因为这些方法是a)太复杂了b)没有按照设计使用的方式使用NSOperation.你们似乎是已经存在的黑客功能.

解决方案是子类化NSOperation并覆盖getter isConcurrent以返回YES.然后,您实现 - (void)start方法并开始异步任务.然后,您负责完成它,这意味着您必须在isFinished和isExecuting上生成KVO通知,以便NSOperationQueue可以知道任务已完成.

(更新:以下是你如何继承NSOperation的方法)(更新2:如果你在后台线程上有代码需要一个代码,你会如何处理NSRunLoop.例如Dropbox Core API)

// 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
Run Code Online (Sandbox Code Playgroud)