KVO с семантикой Run-to-Completion - возможно ли это?

Недавно я столкнулся с проблемами повторного входа в КВО. Чтобы визуализировать проблему, я хотел бы показать минимальный пример. Рассмотрим интерфейс AppDelegate учебный класс

@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (nonatomic) int x;
@end

а также его реализация

@implementation AppDelegate

- (BOOL)          application:(__unused UIApplication *)application
didFinishLaunchingWithOptions:(__unused NSDictionary *)launchOptions
{
    __unused BigBugSource *b = [[BigBugSource alloc] initWithAppDelegate:self];

    self.x = 42;
    NSLog(@"%d", self.x);

   return YES;
}

@end

Неожиданно эта программа выводит 43 на консоль.

Вот почему:

@interface BigBugSource : NSObject {
    AppDelegate *appDelegate;
}
@end

@implementation BigBugSource

- (id)initWithAppDelegate:(AppDelegate *)anAppDelegate
{
    self = [super init];
    if (self) {
        appDelegate = anAppDelegate;
        [anAppDelegate addObserver:self 
                        forKeyPath:@"x" 
                           options:NSKeyValueObservingOptionNew 
                           context:nil];
    }
    return self;
}

- (void)dealloc
{
    [appDelegate removeObserver:self forKeyPath:@"x"];
}

- (void)observeValueForKeyPath:(__unused NSString *)keyPath
                      ofObject:(__unused id)object
                        change:(__unused NSDictionary *)change
                       context:(__unused void *)context
{
    if (appDelegate.x == 42) {
        appDelegate.x++;
    }
}

@end

Как видите, какой-то другой класс (который может быть в стороннем коде, к которому у вас нет доступа) может зарегистрировать невидимого наблюдателя в свойстве. Этот наблюдатель затем вызывается синхронно, всякий раз, когда значение свойства изменилось.

Поскольку вызов происходит во время выполнения другой функции, это вызывает все виды ошибок параллелизма / многопоточности, хотя программа работает в одном потоке. Хуже того, изменение происходит без явного уведомления в клиентском коде (хорошо, вы можете ожидать, что проблемы параллелизма возникают, когда вы устанавливаете свойство...).

Какова наилучшая практика для решения этой проблемы в Objective-C?

  • Есть ли какое-то общее решение для автоматического восстановления семантики выполнения до завершения, означающего, что сообщения KVO-Observation проходят через очередь событий, ПОСЛЕ того, как текущий метод завершает выполнение, и инварианты / постусловия восстанавливаются?

  • Не выставляя какие-либо свойства?

  • Охрана каждой критической функции объекта с помощью логической переменной, чтобы гарантировать невозможность повторного входа? Например: assert(!opInProgress); opInProgress = YES; в начале методов, и opInProgress = NO; в конце методов. Это, по крайней мере, выявит такие ошибки непосредственно во время выполнения.

  • Или можно как-то отказаться от КВО?

Обновить

На основе ответа CRD, вот обновленный код:

BigBugSource

- (void)observeValueForKeyPath:(__unused NSString *)keyPath
                      ofObject:(__unused id)object
                        change:(__unused NSDictionary *)change
                       context:(__unused void *)context
{
    if (appDelegate.x == 42) {
        [appDelegate willChangeValueForKey:@"x"]; // << Easily forgotten
        appDelegate.x++;                          // Also requires knowledge of
        [appDelegate didChangeValueForKey:@"x"];  // whether or not appDelegate  
    }                                             // has automatic notifications
}

AppDelegate

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    if ([key isEqualToString:@"x"]) {
        return NO;
    } else {
        return [super automaticallyNotifiesObserversForKey:key];
    }
}

- (BOOL)          application:(__unused UIApplication *)application
didFinishLaunchingWithOptions:(__unused NSDictionary *)launchOptions
{
    __unused BigBugSource *b = [[BigBugSource alloc] initWithAppDelegate:self];

    [self willChangeValueForKey:@"x"];
    self.x = 42;
    NSLog(@"%d", self.x);    // now prints 42 correctly
    [self didChangeValueForKey:@"x"];
    NSLog(@"%d", self.x);    // prints 43, that's ok because one can assume that
                             // state changes after a "didChangeValueForKey"
    return YES;
}

1 ответ

Вы запрашиваете уведомление об изменении вручную и поддерживается KVO. Это трехэтапный процесс:

  1. Ваш класс переопределяет + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey возврате NO для любой собственности, для которой вы хотите отложить уведомления и отложить до super иначе;
  2. Перед изменением свойства вы звоните [self willChangeValueForKey:key]; а также
  3. Когда вы будете готовы к уведомлению, вы звоните [self didChangeValueForKey:key]

Вы можете создать этот протокол довольно легко, например, легко вести учет ключей, которые вы изменили, и запускать их все перед выходом.

Вы также можете использовать willChangeValueForKey: а также didChangeValueForKey с включенными автоматическими уведомлениями, если вы напрямую изменяете базовую переменную свойства и должны вызывать KVO.

Процесс вместе с примерами описан в документации Apple.

Другие вопросы по тегам