Почему NSOperation отключает автоматическое наблюдение за ключом?
При работе с пользовательским подклассом NSOperation
я заметил, что автоматическая проверка значения ключа отключена методом класса [NSOperation automaticallyNotifiesObserversForKey]
(который возвращает NO
по крайней мере для некоторых путей ключа). Из-за этого код внутри подклассов NSOperation
усеян ручными вызовами willChangeValueForKey:
и didChange…
, как видно во многих образцах кода в Интернете.
Почему это делает NSOperation
? С автоматической поддержкой KVO люди могли просто объявлять свойства для флагов жизненного цикла операции (isExecuting
и т.д.) И запускать события KVO через аксессоров, т.е. следующий код:
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
[self didChangeValueForKey:@"isExecuting"];
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];
... можно заменить следующим:
[self setIsExecuting:NO];
[self setIsFinished:YES];
Есть ли какой-нибудь улов? Я просто переопределял automaticallyNotifiesObserversForKey
, чтобы вернуть YES
, и все выглядит нормально.
Ответы
Ответ 1
Наиболее вероятным объяснением является то, что ключи kvo не соответствуют стандартным соглашениям. Обычно существуют такие методы, как -isExecuting
и -setExecuting:
, где путь ключа @"executing"
. В случае NSOperation путь ключа @"isExecuting"
вместо этого.
Другая возможность заключается в том, что большинство NSOperations фактически не имеют метода с именем -setIsExecuting:
, чтобы изменить это значение. Вместо этого они создают исполняемые/готовые флаги в другом внутреннем состоянии. В этом случае абсолютно необходимо использовать явные уведомления willChange/didChange. Например, если у меня есть NSOperation, который обертывает NSURLConnection, у меня может быть 2 ivars, один с именем data
, в котором хранятся загруженные данные, и один из них с именем connection
, который содержит NSURLConnection, и я могу реализовать геттеры следующим образом:
- (BOOL)isExecuting {
return (connection != nil);
}
- (BOOL)isFinished {
return (data != nil && connection == nil);
}
Теперь мой метод -start
может использовать
[self willChangeValueForKey:@"isExecuting"];
data = [[NSMutableData alloc] init]; // doesn't affect executing, but is used later
connection = [[NSURLConnection connectionWithRequest:request delegate:self] retain];
[self didChangeValueForKey:@"isExecuting"];
чтобы начать выполнение, и
[self willChangeValueForKey:@"isExecuting"];
[self willChangeValueForKey:@"isFinished"];
[connection cancel];
[connection release];
connection = nil;
[self didChangeValueForKey:@"isFinished"];
[self didChangeValueForKey:@"isExecuting"];
.
Ответ 2
Хотя я согласен с тем, что переопределение automaticallyNotifiesObserversForKey
, похоже, работает, но я лично лично отказываюсь от свойств isExecuting
и isFinished
и вместо этого определяю свойства executing
и finished
, которые, как предлагает Кевин, более согласованы с современными соглашениями:
@property (nonatomic, readwrite, getter = isExecuting) BOOL executing;
@property (nonatomic, readwrite, getter = isFinished) BOOL finished;
Затем я пишу настраиваемые настройки для этих двух свойств, которые делают необходимые уведомления isExecuting
и isFinished
:
- (void)setExecuting:(BOOL)executing
{
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)setFinished:(BOOL)finished
{
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}
Это дает:
- более обычное объявление свойства
BOOL
;
- пользовательские сеттеры удовлетворяют странным уведомлениям, которые требуется
NSOperation
; и
- Теперь я могу использовать настройки
executing
и finished
во всей моей реализации операции, не засоряя мой код уведомлениями.
Я должен признаться, что мне нравится элегантность переопределения automaticallyNotifiesObserversForKey
, но я просто беспокоюсь о непреднамеренных последствиях.
Обратите внимание, что если вы сделаете это в iOS 8 или Yosemite, вам также придется явно синтезировать эти свойства в @implementation
:
@synthesize finished = _finished;
@synthesize executing = _executing;
Ответ 3
Я не знаю, почему вы говорите, что NSOperation не может использовать автоматический KVO. Но я просто пытаюсь проверить это, поэтому он может использовать KVO.
[self addObserver:self
forKeyPath:@"isReady"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial
context:&ctxKVO_CSDownloadOperation];
[self addObserver:self
forKeyPath:@"isExecuting"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial
context:&ctxKVO_CSDownloadOperation];
[self addObserver:self
forKeyPath:@"isFinished"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial
context:&ctxKVO_CSDownloadOperation];
[self addObserver:self
forKeyPath:@"isCancelled"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial
context:&ctxKVO_CSDownloadOperation];
...
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == &ctxKVO_CSDownloadOperation) {
NSLog(@"KVO: %@", keyPath);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
Результат:
2017-08-02 14:29:58.831 CSDownloader[77366:5089399] isReady : 1
2017-08-02 14:29:58.831 CSDownloader[77366:5089399] KVO: isReady
2017-08-02 14:29:58.831 CSDownloader[77366:5089399] isExecuting : 0
2017-08-02 14:29:58.831 CSDownloader[77366:5089399] KVO: isExecuting
2017-08-02 14:29:58.831 CSDownloader[77366:5089399] isFinished : 0
2017-08-02 14:29:58.832 CSDownloader[77366:5089399] KVO: isFinished
2017-08-02 14:29:58.832 CSDownloader[77366:5089399] isCancelled : 0
2017-08-02 14:29:58.832 CSDownloader[77366:5089399] KVO: isCancelled
2017-08-02 14:29:58.832 CSDownloader[77366:5089399] isReady : 1
2017-08-02 14:29:58.832 CSDownloader[77366:5089399] KVO: isReady
2017-08-02 14:29:58.833 CSDownloader[77366:5089399] isExecuting : 0
2017-08-02 14:29:58.833 CSDownloader[77366:5089399] KVO: isExecuting
2017-08-02 14:29:58.833 CSDownloader[77366:5089399] isFinished : 0
2017-08-02 14:29:58.833 CSDownloader[77366:5089399] KVO: isFinished
2017-08-02 14:29:58.833 CSDownloader[77366:5089399] isCancelled : 0
2017-08-02 14:29:58.833 CSDownloader[77366:5089399] KVO: isCancelled
2017-08-02 14:29:58.834 CSDownloader[77366:5089399] isReady : 1
2017-08-02 14:29:58.834 CSDownloader[77366:5089399] KVO: isReady
2017-08-02 14:29:58.834 CSDownloader[77366:5089399] isExecuting : 0
Так что я действительно запутался в этом вопросе и ответах...
Ответ 4
NSOperationQueue
не наблюдает isFinished
или isExecuting
, он наблюдает finished
и executing
.
isFinished
- это просто синтезированный get accessor для свойства finished
. Для этого свойства будут отправляться автоматические уведомления о наблюдении за ключевыми значениями, если только ваш подкласс не отказался от автоматических уведомлений KVO, внедрив +automaticallyNotifiesObserversForKey
или +automaticallyNotifiesObserversOf<Key>
, чтобы вернуть NO. Если вы не отказались от автоматических уведомлений KVO, вам не нужно выполнять ручные уведомления, используя will/DidChangeValueForKey:
. В вашем случае вы отправляете ручные уведомления для isFinished
и isExecuting
, которые не являются ключевыми путями, которые наблюдаются NSOperationQueue
.
TL; DR: Это не ключевые пути, которые ищет NSOperationQueue.
![These are not the key paths you are looking for]()
executing
и finished
являются правильными путями ключей, и они должны отправлять автоматические уведомления KVO.
Если вы являетесь по-настоящему параноидальным о KVO и хотите отправлять уведомления для путей доступа get accessor, таких как isFinished
, зарегистрируйте свое свойство как зависимость пути ключа:
+ (NSSet *) keyPathsForValuesAffectingIsFinished {
NSSet *result = [NSSet setWithObject:@"finished"];
return result;
}