Подтвердить что ты не робот

Почему 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, и все выглядит нормально.

4b9b3361

Ответ 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;
}