18

我发布这个问题是因为我在这个主题上看到了很多困惑,因此我花了几个小时调试 NSOperation 子类。

问题是当你执行异步方法时,NSOperation 对你没有多大好处,这些方法在异步回调完成之前实际上是不完整的。

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

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

您不能阻塞主线程,否则 UI 将被锁定,因此您有两种选择。

1) 创建一个 NSOperation 并将其添加到具有以下签名的 NSOperationQueue 中:

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

祝你好运。异步操作通常需要一个运行循环,因此除非您将 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;
}

好的 - 这绝对行不通 - 我们已经解决了这个问题!

2)这种方法的第二个问题是,在 runLoops 必须正确终止的情况下(或者实际上从回调中的外部方法调用中完全终止),可以说上面的方法确实有效(它没有)

当我调用它时,让我们在主线程中假设第二个 Im,除非我希望 UI 锁定一段时间,并且不绘制任何东西,否则我不能在 NSOperationQueue addOperation 方法上说“waitUntilFinished:YES”......

那么如何在不锁定主线程的情况下完成与 waitUntilFinished:YES 相同的行为呢?

由于 Cocoa 中有很多关于 runLoops、NSOperationQueues 和异步行为的问题,我将发布我的解决方案作为对这个问题的回答。

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

4

3 回答 3

16

问题#1的答案

我有一个 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"
//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;
}

问题 #2 的答案- 你不能使用

[NSOperationQueue addOperations:.. waitUntilFinished:YES]

因为您的主线程不会更新,但是您还有几个 OTHER 操作在此 NSOperation 完成之前不得执行,并且它们中的任何一个都不应阻塞主线程。

进入...

dispatch_semaphore_t

如果你有几个依赖的 NSOperation 需要从主线程启动,你可以将调度信号量传递给 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];

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

}

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

  1. 运行循环将停止,允许 NSOperation 完成(否则不会)

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

  3. NSOperation 将完成并被释放

如果您使用方法 2,您可以等待从 NSOperationQueue 调用的任意异步方法,知道它们将完成运行循环,并且您可以以任何您喜欢的方式链接回调,同时永远不会阻塞主线程。

于 2012-09-05T16:19:04.703 回答
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
于 2014-03-26T13:43:54.703 回答
0

我不确定为什么你会想要 NSOperation 的所有开销只用于运行循环,但我想如果你使用的是操作队列设计,那么它可能会很有用。我这么说的原因通常是你只需在后台执行一个选择器并从那里调用 CFRunLoopRun 。

除此之外,下面是一个使用运行循环的示例 NSOperation 子类。只需将其子类化并覆盖 willRun 并调用需要运行循环才能工作的方法。一旦所有调用的方法都完成了,所有的运行循环源都被处理了——操作将自动结束。您可以通过在 willRun 方法的延迟后放置一个简单的执行选择器并在 completeOperation 中放置一个断点来测试它,您将看到操作将持续到完成执行所需的时间。此外,如果您在延迟之后执行其他操作,那么操作将继续运行。正如我所说,只要有一些东西需要运行循环才能运行,它就会一直运行,即使它们是在它启动后添加的。

不需要停止方法,因为一旦一切都完成并且没有更多的源可以处理,它将自动结束。

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

到底是什么,这也是一个示例子类:-)

@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

只需像这样测试它:

TestLoop* t = [[TestLoop alloc] init];
[t start];
于 2016-01-25T19:07:57.247 回答