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

ご覧のとおり、いくつかの異なるクラス (アクセス権のないサードパーティ コードにある可能性があります) が、目に見えないオブザーバーをプロパティに登録する場合があります。このオブザーバーは、プロパティの値が変更されるたびに同期的に呼び出されます。

呼び出しは別の関数の実行中に発生するため、プログラムは単一のスレッドで実行されますが、あらゆる種類の同時実行/マルチスレッドのバグが発生します。さらに悪いことに、クライアントコードで明示的な通知なしに変更が行われます (OK、プロパティを設定するたびに同時実行の問題が発生することが予想されます...)。

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 によってサポートされています。これは 3 段階のプロセスです。

  1. あなたのクラスは、通知を延期したいプロパティの+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey戻り値をオーバーライドし、それ以外の場合は延期します。NOsuper
  2. [self willChangeValueForKey:key]プロパティを変更する前に;を呼び出します。と
  3. 通知が発生する準備ができたら、電話します[self didChangeValueForKey:key]

このプロトコルを非常に簡単に構築できます。たとえば、変更したキーの記録を保持し、終了する前にそれらすべてをトリガーするのは簡単です。

プロパティのバッキング変数を直接変更し、KVO をトリガーする必要がある場合は、自動通知をオンにしてwillChangeValueForKey:andを使用することもできます。didChangeValueForKey

このプロセスと例は、Apple のドキュメントに記載されています。

于 2012-08-07T19:33:53.603 に答える