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. Это трехэтапный процесс:
- Ваш класс переопределяет
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey
возвратеNO
для любой собственности, для которой вы хотите отложить уведомления и отложить доsuper
иначе; - Перед изменением свойства вы звоните
[self willChangeValueForKey:key]
; а также - Когда вы будете готовы к уведомлению, вы звоните
[self didChangeValueForKey:key]
Вы можете создать этот протокол довольно легко, например, легко вести учет ключей, которые вы изменили, и запускать их все перед выходом.
Вы также можете использовать willChangeValueForKey:
а также didChangeValueForKey
с включенными автоматическими уведомлениями, если вы напрямую изменяете базовую переменную свойства и должны вызывать KVO.
Процесс вместе с примерами описан в документации Apple.