如何在NSOperation中实现NSRunLoop(How to implement an NSRunLoop inside an NSOperation)

IPhone IT屋
百度翻译此文   有道翻译此文
问 题

Im posting this question because I have seen a lot of confusion over this topic and I spent several hours debugging NSOperation subclasses as a result.

The problem is that NSOperation doesnt do you much good when you execute Asynchronous methods which are not actually complete until the asynchronous callback completes.

If the NSOperation itself is the callback delegate it may not even be sufficient to properly complete the operation due to the callback occurring on a different thread.

Lets say you are in the main thread and you create an NSOperation and add it to an NSOperationQueue the code inside the NSOperation fires an Asynchronous call which calls back to some method on the AppDelegate or a view controller.

You cant block the main thread or the UI will lock up, so you have two options.

1) Create an NSOperation and add it to the NSOperationQueue with the following signature:

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

Good luck with that. Asynchronous operations usually require a runloop so it wont work unless you subclass NSOperation or use a block, but even a block wont work if you have to "complete" the NSOperation by telling it when the callback has finished.

So...you subclass NSOperation with something akin to the following so the callback can tell the operation when its finished:

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

OK - This absolutely does not work - we got that out of the way!

2) The second problem with this method is that lets say the above actually worked (which it does not) in cases where runLoops have to terminate properly, (or actually terminate at all from an external method call in a callback)

Lets assume for a second Im in the main thread when I call this, unless I want the UI to lock up for a bit, and not paint anything, I cant say "waitUntilFinished:YES" on the NSOperationQueue addOperation method...

So how do I accomplish the same behavior as waitUntilFinished:YES without locking up the main thread?

Since there are so many questions regarding runLoops, NSOperationQueues and Asynch behavior in Cocoa, I will post my solution as an answer to this question.

Note that Im only answering my own question because I checked meta.stackoverflow and they said this is acceptable and encouraged, I hope the answer that follows helps people to understand why their runloops are locking up in NSOperations and how they can properly complete NSOperations from external callbacks. (Callbacks on other threads)

解决方案

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 pass a 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 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];

...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 NSOperation 3 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.

本文地址:IT屋 » How to implement an NSRunLoop inside an NSOperation

问 题

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



问题在于NSOperation在异步回调完成之前执行非实际完成的异步方法时,你做得不好。



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



让我们说你在主线程中并创建一个NSOperation并将其添加到NSOperationQueue,NSOperation中的代码触发异步调用,该调用回调AppDelegate或视图控制器上的某些方法。



您无法阻止主线程或者UI会锁定,因此您有两种选择。



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



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



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



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



  //你创建一个NSOperation子类,它包含一个
//保持runloop的主方法如下
//你的NSOperation子类有一个名为“complete”的BOOL字段

- (void)main
{

NSRunLoop * runLoop = [NSRunLoop currentRunLoop];

//我做了一些对appDelegate或任何其他类(非常常见)的异步回调的东西

while(!complete&& [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

}

//我还有一个setter,回调方法可以在此操作上调用
//告诉操作它已完成,
//所以它完成,结束runLoop并结束操作

- (void)setComplete {
complete = true;
}

//我覆盖isFinished,以便观察者可以看到我什么时候完成
// - 因为我的“完整”字段是我的实例的本地

- (BOOL)isFinished
{
返回完成;
}


好 - 这绝对不起作用 - 我们得到了顺便说一下!



2)这个方法的第二个问题就是让我们说上面的实际工作(它没有)在runLoops必须正确终止的情况下(或实际上从回调中的外部方法调用终止)



让我们假设主线程中的第二个Im当我打电话给这个,除非我想让UI锁定一点,而不是画任何东西,我不能在NSOperationQueue addOperation方法上说“waitUntilFinished:YES”......



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



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



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


解决方案

问题#1的答案



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



以下代码从上面修改



  //你创建一个NSOperation子类它包含一个main方法
//保持runloop如下
//你的NSOperation子类有一个名为“complete”的BOOL字段
// ADDED:你的NSOperation子类有一个名为“stopRunLoop”的BOOL字段
// ADDED:您的NSOperation子类有一个名为“myThread”的NSThread *字段
- (void)main
{
myThread = [NSThread currentThread];
NSRunLoop * runLoop = [NSRunLoop currentRunLoop];

//我做了一些对appDelegate或任何其他类(非常常见)的异步回调的东西

while(!stopRunLoop&& [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

//在一个NSOperation中,另一个线程无法设置完成
//即使使用方法调用操作
//这是必需的,或实际调用main和$的线程b $ b // KVO观察将看不到值变化
//您也可能需要在设置完成之前进行后期处理。
//如果你只是在
// runloop之后的任何东西上设置完成就不会被执行。
//确保你真的完成了。

complete = YES;

}


- (无效)internalComplete
{
stopRunloop = YES;
}

//这需要停止runLoop,
//只是设置另一个线程的值不起作用,
//自那个线程以来创建了NSOperation子类
//将成员字段复制到运行main()方法的线程的
//堆栈中。

- (void)setComplete {
[self performSelector:@selector(internalComplete)onThread:myThread withObject:nil waitUntilDone:NO];
}

//覆盖isFinished与
之前相同 - (BOOL)isFinished
{
return complete;
}


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



  [NSOperationQueue addOperations:.. waitUntilFinished:YES] 


因为你的主线程不会更新,但是你还有几个其他的操作,其中
必须在NSOperation完成之前不能执行,并且它们中的任何一个都不应该阻止主线程。



输入...



  dispatch_semaphore_t 


如果您需要从主线程启动多个相关NSOperations,则可以传递
a dispatch信号量对于NSOperation,记住这些是NSOperation主方法中的异步调用,因此NSOperation子类需要等待这些回调完成。
另外从回调链接的方法可能是一个问题。



通过从主线程传入信号量,你可以使用[NSOperation addOperations:... waitUntilFinished:在你的回调全部完成之前仍然阻止其他操作执行。



创建NSOperation的主线程的代码



  //一次只运行一个操作
dispatch_semaphore_t mySemaphore = dispatch_semaphore_create(1);

//在创建时将你的信号量传递给NSOperation
myOperation = [[YourCustomNSOperation alloc] initWithSemaphore:mySemaphore] autorelease];

//调用操作
[myOperationQueue addOperations:@ [myOperation] waitUntilFinished:NO];


... NSOperation的代码



  //在自定义NSOperation的main方法中 - (如上所示)在
之前添加此调用//您的方法执行任何操作
//我的自定义NSOperation子类有一个类型为dispatch_semaphore_t的字段
//名为“mySemaphore”

- (void)main
{
myThread = [NSThread currentThread];
NSRunLoop * runLoop = [NSRunLoop currentRunLoop];

//获取信号量或等到其可用的
dispatch_semaphore_wait(mySemaphore,DISPATCH_TIME_FOREVER);

//我做了一些对appDelegate或任何其他类(非常常见)的异步回调的东西

while(!stopRunLoop&& [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

//释放信号量
dispatch_semaphore_signal(mySemaphore);

complete = YES;

}


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




  1. 将停止运行runloop以允许NSOperation完成(否则不会)


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


  3. NSOperation将完成并被取消分配




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


本文地址:IT屋 » 如何在NSOperation中实现NSRunLoop

官方微信
扫一扫关注IT屋
微信公众号搜索 “ IT屋 ” ,选择关注
与百万开发者在一起