3

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

1 回答 1

3

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

  1. 您的课程将覆盖您希望推迟通知的任何属性的+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey返回,并推迟到其他情况;NOsuper
  2. 在更改您调用的属性之前[self willChangeValueForKey:key];和
  3. 当你准备好通知发生时,你打电话[self didChangeValueForKey:key]

您可以很容易地在此协议上进行构建,例如,很容易记录您更改的密钥并在退出之前触发它们。

如果您直接更改属性的支持变量并需要触发 KVO,还可以使用willChangeValueForKey:didChangeValueForKey打开自动通知。

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

于 2012-08-07T19:33:53.603 回答