【问题标题】:KVO with Run-to-Completion semantics - Is it possible?具有 Run-to-Completion 语义的 KVO - 有可能吗?
【发布时间】:2012-08-07 16:21:46
【问题描述】:

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

【问题讨论】:

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


    【解决方案1】:

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

    1. 您的类将覆盖 + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey,为您希望推迟通知的任何属性返回 NO,否则推迟到 super
    2. 在更改属性之前,请致电[self willChangeValueForKey:key];和
    3. 当您准备好接收通知时,请致电 [self didChangeValueForKey:key]

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

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

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

    【讨论】:

    • 这已经改善了这种情况。但是,仍然存在问题。考虑这样一种情况,您有一个属于第三方库并使用自动通知的类。当您更改此类实例的属性时,如果另一个对象已将自己注册为观察者,则会再次出现重入问题。第二个问题是,如果您从外部更改属性,您还必须调用willChangedidChange,这使得代码容易出错,因为它很容易被遗忘。有没有真正支持运行到完成语义的解决方案?
    • @Etan - 对于第三方代码,您真的必须让他们按照他们认为合适的方式行事。对于您的代码,您可以通过一些工作保留特定类的通知,直到您发布它们 - 关闭自动通知并将will/didChange 添加到您的设置器中,您可以回到自动通知的等效状态;但是,如果您现在添加一个标志(使用一个属性,例如 1holdNotifications` 来(取消)设置它),当设置时会导致您的设置器将 willChange 排队,并且当 unset 触发任何排队的 willChange 时,您将获得我认为的语义你在追求。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-07-28
    • 2022-11-25
    • 2011-12-11
    • 1970-01-01
    • 2016-08-21
    • 2012-03-17
    • 1970-01-01
    相关资源
    最近更新 更多