我最近碰到KVO的重入問題。爲了想象這個問題,我想舉一個最簡單的例子。考慮的AppDelegate
類帶運行至完成語義的KVO - 有可能嗎?
@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
沒想到,這個程序打印到控制檯。
這裏的原因:
@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中解決此問題的最佳做法是什麼?
有一些常見的解決方案,以恢復自動運行至完成的語義,這意味着國際志願者組織觀察消息通過事件隊列之後,目前的方法執行完畢和不變/後置恢復?
不公開任何屬性?
用布爾變量保護對象的每個關鍵函數以確保重入不可能? 例如:方法開始時爲
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;
}
這已經改善了這種情況。但是,仍然存在問題。考慮一種情況,即您有一個屬於第三方庫並且使用自動通知的類。當您更改此類的實例的屬性時,如果另一個對象已將自己註冊爲觀察者,則重入問題會再次出現。第二個問題是,如果從外部更改屬性,則還必須調用'willChange'和'didChange',從而使代碼容易出錯,因爲它很容易被遺忘。有沒有解決方案,真正支持運行到完成語義? – Etan 2012-08-08 08:31:05
@Etan - 對於第三方代碼,您實際上必須讓它們按照他們認爲合適的方式行事。爲了讓你的代碼做一點工作,你可以爲特定的類保留通知,直到你釋放它們爲止 - 關閉自動通知並向你的設置者添加'will/didChange'讓你回到自動通知的等價物;然而,如果你現在添加一個標誌(使用一個屬性,比如說1holdNotifications'來設置它),當set被設置時,你的設置者將對willChange進行排隊,並且當unset關閉任何排隊的willChange時,你會得到我認爲的語義你在之後。 – CRD 2012-08-08 08:58:51