最近、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;
}