具有Run-to-Completion语义的KVO - 有可能吗?

时间:2012-08-07 16:21:47

标签: objective-c cocoa architecture key-value-observing reentrancy

我最近遇到了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;
}

1 个答案:

答案 0 :(得分:3)

您要求的是手动更改通知,并得到KVO的支持。这是一个三阶段的过程:

  1. 您的课程将覆盖+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey返回NO以查找您希望推迟通知的任何媒体资源,否则将推迟至super;
  2. 在更改房产之前,请致电[self willChangeValueForKey:key];和
  3. 当您准备好发出通知时,请致电[self didChangeValueForKey:key]
  4. 您可以非常轻松地构建此协议,例如您可以轻松记录已更改的密钥并在退出之前将其全部触发。

    如果直接更改属性的后备变量并需要触发KVO,您还可以使用willChangeValueForKey:didChangeValueForKey在上启用自动通知。

    Apple的documentation中描述了该过程以及示例。