3

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

回答

3

您要求的是手動更改通知,並且由KVO支持。這是一個三個階段的過程:

  1. 你的類重寫+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey返回NO您要推遲通知的任何財產,推遲到super否則;
  2. 更改屬性前,請致電[self willChangeValueForKey:key];和
  3. 當你準備好要發生的通知,你叫[self didChangeValueForKey:key]

您可以構建在該協議很容易,例如在退出之前,很容易記錄已更改的鍵並觸發它們。

您還可以使用willChangeValueForKey:didChangeValueForKey自動開啓通知如果直接改變屬性的後盾變量,需要觸發志願。

蘋果的documentation中描述了這個過程和一個例子。

+0

這已經改善了這種情況。但是,仍然存在問題。考慮一種情況,即您有一個屬於第三方庫並且使用自動通知的類。當您更改此類的實例的屬性時,如果另一個對象已將自己註冊爲觀察者,則重入問題會再次出現。第二個問題是,如果從外部更改屬性,則還必須調用'willChange'和'didChange',從而使代碼容易出錯,因爲它很容易被遺忘。有沒有解決方案,真正支持運行到完成語義? – Etan 2012-08-08 08:31:05

+0

@Etan - 對於第三方代碼,您實際上必須讓它們按照他們認爲合適的方式行事。爲了讓你的代碼做一點工作,你可以爲特定的類保留通知,直到你釋放它們爲止 - 關閉自動通知並向你的設置者添加'will/didChange'讓你回到自動通知的等價物;然而,如果你現在添加一個標誌(使用一個屬性,比如說1holdNotifications'來設置它),當set被設置時,你的設置者將對willChange進行排隊,並且當unset關閉任何排隊的willChange時,你會得到我認爲的語義你在之後。 – CRD 2012-08-08 08:58:51

相關問題