Как реализовать NSRunLoop внутри NSOperation
Я публикую этот вопрос, потому что я видел много путаницы по этой теме, и я провел несколько часов отладки подклассов NSOperation в результате.
Проблема заключается в том, что NSOperation не очень хорошо работает при выполнении асинхронных методов, которые на самом деле не завершены до завершения асинхронного обратного вызова.
Если сам NSOperation является делегатом callback, он может даже оказаться недостаточным для правильного завершения операции из-за обратного вызова, происходящего в другом потоке.
Предположим, что вы находитесь в основном потоке, и вы создаете NSOperation и добавляете его в NSOperationQueue, код внутри NSOperation запускает асинхронный вызов, который обращается к некоторому методу в AppDelegate или контроллере представления.
Вы не можете заблокировать основной поток или пользовательский интерфейс заблокируется, поэтому у вас есть два варианта.
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 в основном потоке, когда я это вызвал, если я не хочу, чтобы пользовательский интерфейс блокировался немного, и ничего не нарисовал, я не могу сказать "waitUntilFinished: YES" в методе addOperation NSOperationQueue..
Итак, как мне выполнить то же поведение, что и waitUntilFinished: YES, не блокируя основной поток?
Так как есть много вопросов относительно runLoops, NSOperationQueues и поведения Asynch в Cocoa, я отправлю свое решение в качестве ответа на этот вопрос.
Обратите внимание, что Im только отвечает на мой собственный вопрос, потому что я проверил meta.stackoverflow, и они сказали, что это приемлемо и приветствуется. Я надеюсь, что ответ, который следует за, помогает людям понять, почему их runloops блокируются в NSOperations и то, как они могут правильно выполнять NSOperations из внешних обратных вызовов. (Обратные вызовы для других потоков)
Ответы
Ответ 1
Ответ на проблему №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]
Поскольку ваш основной поток не будет обновляться, но у вас также есть несколько ДРУГИХ операций, которые
не должен выполняться до тех пор, пока это NSOperation не будет завершено, и NONE из них должен заблокировать основной поток.
Введите...
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;
}
Когда ваш метод обратного вызова в другом потоке вызывает setComplete в NSOperation
3 вещи произойдут,
-
Запуск runloop будет остановлен, чтобы выполнить NSOperation (что иначе не будет)
-
Семафор будет выпущен, позволяя другим операциям совместно использовать семафор для запуска
-
NSOperation завершится и будет отменен
Если вы используете метод 2, вы можете ждать произвольные асинхронные методы, вызванные из NSOperationQueue,
знайте, что они завершат runloop, и вы можете привязывать обратные вызовы любым способом, как вам нравится,
никогда не блокируя основной поток.
Ответ 2
Я не читал эти ответы ни в каких подробностях, потому что эти подходы являются a) слишком сложными и b) не используют NSOperation так, как он предназначен для использования. Вы, ребята, видите, что есть хакерская функциональность, которая уже существует.
Решение заключается в подклассе NSOperation и переопределении getter isConcurrent для возврата YES. Затем вы реализуете метод запуска (void) и начинаете свою асинхронную задачу. Затем вы отвечаете за его завершение, то есть вы должны генерировать уведомления KVO на isFinished и isExecuting, чтобы NSOperationQueue мог знать, что задача выполнена.
(UPDATE: здесь, как вы должны подклассифицировать NSOperation)
(ОБНОВЛЕНИЕ 2: добавлено, как вы будете обрабатывать NSRunLoop, если у вас есть код, который требует его при работе в фоновом потоке. Например, API-интерфейс Dropbox)
// 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
Ответ 3
Я не уверен, почему вы хотите все накладные расходы 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];