我最近遇到了 KVO 的重入问题。为了形象化这个问题,我想展示一个最小的例子。考虑一个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;
的结尾。这至少会在运行时直接揭示这些错误。或者是否有可能以某种方式退出 KVO?
更新
根据 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;
}