我最近遇到了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;
}
答案 0 :(得分:3)
您要求的是手动更改通知,并得到KVO的支持。这是一个三阶段的过程:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey
返回NO
以查找您希望推迟通知的任何媒体资源,否则将推迟至super
; [self willChangeValueForKey:key]
;和[self didChangeValueForKey:key]
您可以非常轻松地构建此协议,例如您可以轻松记录已更改的密钥并在退出之前将其全部触发。
如果直接更改属性的后备变量并需要触发KVO,您还可以使用willChangeValueForKey:
和didChangeValueForKey
在上启用自动通知。
Apple的documentation中描述了该过程以及示例。