我发布这个问题,因为我看到很多关于这个主题的混淆,因此我花了几个小时调试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;
}
好 - 这绝对不起作用 - 我们已经解决了这个问题!
2)这个方法的第二个问题是,在runLoops必须正确终止(或实际上从外部终止)的情况下,让我们说上面的实际工作(它没有)回调中的方法调用)
让我假设在我调用它时主线程中的第二个Im,除非我想让UI锁定一点,而不是绘制任何东西,我不能在NSOperationQueue addOperation方法上说“waitUntilFinished:YES”。
那么如何实现与waitUntilFinished相同的行为:YES而不锁定主线程?
由于在Cocoa中有关于runLoops,NSOperationQueues和Asynch行为的问题很多,我将发布我的解决方案作为这个问题的答案。
请注意,我只回答了我自己的问题,因为我检查了meta.stackoverflow并且他们说这是可以接受和鼓励的,我希望后面的答案可以帮助人们理解为什么他们的runloops锁定了NSOperations以及他们如何从外部回调中正确完成NSOperations。 (其他线程的回调)
答案 0 :(得分: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;
}
回答问题#2 - 你无法使用
[NSOperationQueue addOperations:.. waitUntilFinished:YES]
因为您的主线程不会更新,但您还有几个OTHER操作 在NSOperation完成之前一定不能执行,并且它们中的任何一个都不应该阻塞主线程。
输入...
dispatch_semaphore_t
如果您需要从主线程启动多个从属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];
... 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上的setComplete时 会发生三件事,
runloop将被停止,允许NSOperation完成(否则不会)
信号量将被释放,允许共享信号量的其他操作运行
NSOperation将完成并取消分配
如果使用方法2,则可以等待从NSOperationQueue调用的任意异步方法, 知道他们将完成runloop,你可以用你喜欢的任何方式链接回调, 从不阻止主线程。
答案 1 :(得分: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
答案 2 :(得分: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];