NSKeyValueObserving非正式協議定義了一種機制,它允許對象去監聽其它對象的某個屬性的修改。
我們可以監聽一個對象的屬性,包括簡單屬性,一對一的關系,和一對多的關系。一對多關系的監聽者會被告知集合變更的類型,以及哪些對象參與了變化。
NSObject提供了一個NSKeyValueObserving協議的默認實現,它為所有對象提供了一種自動發送修改通知的能力。我們可以通過禁用自動發送通知并使用這個協議提供的方法來手動實現通知的發送,以便更精確地去處理通知。
在這里,我們將通過具體的實例來看看NSKeyValueObserving提供了哪些方法。我們的基礎代碼如代碼清單1所示:
代碼清單1:示例基礎代碼
#PRagma mark - PersonObject@interface PersonObject : NSObject@end@implementation PersonObject- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog(@"keyPath = %@, change = %@, context = %s", keyPath, change, (char *)context);}@end#pragma mark - BankObject@interface BankObject : NSObject@property (nonatomic, assign) int accountBalance;@property (nonatomic, copy) NSString *bankCodeEn;@property (nonatomic, strong) NSMutableArray *departments;@end
在這段代碼中,我們定義一兩個類,一個是PersonObject類,這個類的對象在下面將充當觀察者的角色。另一個是BankObject類,我們在這個類中定義了三個屬性,作為被監聽的屬性。由于NSObject類已經實現了NSKeyValueObserving協議,所以我們不需要再顯式地去讓我們的類實現這個協議。
接下來,我們便來看看NSKeyValueObserving協議有哪些功能。
注冊/移除觀察者
要讓一個對象監聽另一個對象的屬性的變化,首先需要將這個對象注冊為相關屬性的觀察者,我們可以使用以下方法來實現:
- (void)addObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
這個方法帶有四個參數,描述如下:
- anObserver:觀察者對象,這個對象必須實現observeValueForKeyPath:ofObject:change:context:方法,以響應屬性的修改通知。
- keyPath:被監聽的屬性。這個值不能為nil。
- options:監聽選項,這個值可以是NSKeyValueObservingOptions選項的組合。關于監聽選項,我們會在下面介紹。
- context:任意的額外數據,我們可以將這些數據作為上下文數據,它會傳遞給觀察者對象的observeValueForKeyPath:ofObject:change:context:方法。這個參數的意義在于用于區分同一對象監聽同一屬性(從屬于同一對象)的多個不同的監聽。我們將在下面看到。
監聽選項是由枚舉NSKeyValueObservingOptions定義的,是傳入-addObserver:forKeyPath:options:context:方法中以確定哪些值將被傳到-observeValueForKeyPath:ofObject:change:context:方法中。這個枚舉的定義如下:
enum { // 提供屬性的新值 NSKeyValueObservingOptionNew = 0x01, // 提供屬性的舊值 NSKeyValueObservingOptionOld = 0x02, // 如果指定,則在添加觀察者的時候立即發送一個通知給觀察者, // 并且是在注冊觀察者方法返回之前 NSKeyValueObservingOptionInitial = 0x04, // 如果指定,則在每次修改屬性時,會在修改通知被發送之前預先發送一條通知給觀察者, // 這與-willChangeValueForKey:被觸發的時間是相對應的。 // 這樣,在每次修改屬性時,實際上是會發送兩條通知。 NSKeyValueObservingOptionPrior = 0x08 };typedef NSUInteger NSKeyValueObservingOptions;
需要注意的是,當設定了NSKeyValueObservingOptionPrior選項時,第一條通知不會包含NSKeyValueChangeNewKey。當觀察者自身的KVO需要為自己的某個屬性調用-willChange…方法,而這個屬性的值又依賴于被觀察對象的屬性時,我們可以使用這個選項。
另外,在添加觀察者時還有兩點需要注意的是:
- 調用這個方法時,兩個對象(即觀察者對象及屬性所屬的對象)都不會被retain。
- 可以多次調用這個方法,將同一個對象注冊為同一屬性的觀察者(所有參數可以完全相同)。這時,即便在所有參數一致的情況下,新注冊的觀察者并不會替換原來觀察者,而是會并存。這樣,當屬性被修改時,兩次監聽都會響應。
對于第2點,我們在代碼清單2中來驗證一下:
代碼清單2:驗證多次使用相同參數來添加觀察者的實際效果
PersonObject *personInstance = [[PersonObject alloc] init];BankObject *bankInstance = [[BankObject alloc] init];[bankInstance addObserver:personInstance forKeyPath:@"accountBalance" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];[bankInstance addObserver:personInstance forKeyPath:@"accountBalance" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];[bankInstance addObserver:personInstance forKeyPath:@"accountBalance" options:NSKeyValueObservingOptionNew context:"person instance"];[bankInstance addObserver:personInstance forKeyPath:@"accountBalance" options:NSKeyValueObservingOptionNew context:"person instance 2"];bankInstance.accountBalance = 10;
(注,以上代碼為在MRC環境下調用,確保personInstance和bankInstance不會被釋放。)
這段代碼的輸出如下所示:
keyPath = accountBalance, change = { kind = 1; new = 10;}, context = person instance 2keyPath = accountBalance, change = { kind = 1; new = 10;}, context = person instancekeyPath = accountBalance, change = { kind = 1; new = 10; old = 0;}, context = (null)keyPath = accountBalance, change = { kind = 1; new = 10; old = 0;}, context = (null)
可以看到KVO為每次注冊都調用了一次監聽處理操作。所以多次調用同樣的注冊操作會產生多個觀察者。另外,多個觀察者之間的observeValueForKeyPath:ofObject:change:context:方法調用順序是按照先進后出的順序來的(所有的監聽信息都是放在一個數組中的,我們將在下面了解到)。
一個良好的實踐是在觀察者不再需要監聽屬性變化時,必須調用removeObserver:forKeyPath:或removeObserver:forKeyPath:context:方法來移除觀察者,這兩個方法的聲明如下:
- (void)removeObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context
這兩個方法會根據傳入的參數(主要是keyPath和context)來移除觀察者。如果observer沒有監聽keyPath屬性,則調用這兩個方法會拋出異常。大家可以試一下,程序會果斷的崩潰。并報類似于以下的錯誤:
*** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <PersonObject 0x7ff541534e20> for the key path "accountBalance" from <BankObject 0x7ff541528430> because it is not registered as an observer.'
所以,我們必須確保先注冊了觀察者,才能調用移除方法。
那如果我們忘記調用移除觀察者方法,會怎么樣呢?我們來制造一個場景,看看會是什么結果。還是使用上面的代碼,只不過這次我們在ARC下來測試:
代碼清單3:未移除觀察者的影響
- (void)testKVO { PersonObject *personInstance = [[PersonObject alloc] init]; BankObject *bankInstance = [[BankObject alloc] init]; [bankInstance addObserver:personInstance forKeyPath:@"accountBalance" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL]; bankInstance.accountBalance = 20;}
其輸出結果如下所示:
keyPath = accountBalance, change = { kind = 1; new = 20; old = 0;}, context = (null)*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x7fc88047e7e0 of class BankObject was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x7fc880770fa0> (<NSKeyValueObservance 0x7fc880771850: Observer: 0x7fc8804737a0, Key path: accountBalance, Options: <New: YES, Old: YES, Prior: NO> Context: 0x0, Property: 0x7fc88076edd0>)'......
程序在調用一次KVO后,很爽快地崩潰了。給我們的解釋是bankInstance被釋放了,但KVO中仍然還有關于它的注冊信息。實際上,我們上面說過,在添加觀察者的時候,觀察者對象與被觀察屬性所屬的對象都不會被retain,然而在這些對象被釋放后,相關的監聽信息卻還存在,KVO做的處理是直接讓程序崩潰。
處理屬性修改通知
當被監聽的屬性修改時,KVO會發出一個通知以告知所有監聽這個屬性的觀察者對象。而觀察者對象必須實現 -observeValueForKeyPath:ofObject:change:context:方法,來對屬性修改通知做相應的處理。這個方法的聲明如下:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
這個方法有四個參數,描述如下:
- keyPath:即被觀察的屬性,與參數object相關。
- object:keyPath所屬的對象。
- change:這是一個字典,它包含了屬性被修改的一些信息。這個字典中包含的值會根據我們在添加觀察者時設置的options參數的不同而有所不同。
- context:這個值即是添加觀察者時提供的上下文信息。
在我們的示例中,這個方法的實現是打印一些基本的信息。如代碼清單1所示。
對于第三個參數,我們通常稱之為變化字典(Change Dictionary),它記錄了被監聽屬性的變化情況。我們可以通過以下key來獲取我們想要的信息:
// 屬性變化的類型,是一個NSNumber對象,包含NSKeyValueChange枚舉相關的值NSString *const NSKeyValueChangeKindKey;// 屬性的新值。當NSKeyValueChangeKindKey是 NSKeyValueChangeSetting,// 且添加觀察的方法設置了NSKeyValueObservingOptionNew時,我們能獲取到屬性的新值。// 如果NSKeyValueChangeKindKey是NSKeyValueChangeInsertion或者NSKeyValueChangeReplacement,// 且指定了NSKeyValueObservingOptionNew時,則我們能獲取到一個NSArray對象,包含被插入的對象或// 用于替換其它對象的對象。NSString *const NSKeyValueChangeNewKey;// 屬性的舊值。當NSKeyValueChangeKindKey是 NSKeyValueChangeSetting,// 且添加觀察的方法設置了NSKeyValueObservingOptionOld時,我們能獲取到屬性的舊值。// 如果NSKeyValueChangeKindKey是NSKeyValueChangeRemoval或者NSKeyValueChangeReplacement,// 且指定了NSKeyValueObservingOptionOld時,則我們能獲取到一個NSArray對象,包含被移除的對象或// 被替換的對象。NSString *const NSKeyValueChangeOldKey;// 如果NSKeyValueChangeKindKey的值是NSKeyValueChangeInsertion、NSKeyValueChangeRemoval// 或者NSKeyValueChangeReplacement,則這個key對應的值是一個NSIndexSet對象,// 包含了被插入、移除或替換的對象的索引NSString *const NSKeyValueChangeIndexesKey;// 當指定了NSKeyValueObservingOptionPrior選項時,在屬性被修改的通知發送前,// 會先發送一條通知給觀察者。我們可以使用NSKeyValueChangeNotificationIsPriorKey// 來獲取到通知是否是預先發送的,如果是,獲取到的值總是@(YES)NSString *const NSKeyValueChangeNotificationIsPriorKey;
其中,NSKeyValueChangeKindKey的值取自于NSKeyValueChange,它的值是由以下枚舉定義的:
enum { // 設置一個新值。被監聽的屬性可以是一個對象,也可以是一對一關系的屬性或一對多關系的屬性。 NSKeyValueChangeSetting = 1, // 表示一個對象被插入到一對多關系的屬性。 NSKeyValueChangeInsertion = 2, // 表示一個對象被從一對多關系的屬性中移除。 NSKeyValueChangeRemoval = 3, // 表示一個對象在一對多的關系的屬性中被替換 NSKeyValueChangeReplacement = 4};typedef NSUInteger NSKeyValueChange;
通知觀察者屬性的變化
通知觀察者的方式有自動與手動兩種方式。
默認情況下是自動發送通知,在這種模式下,當我們修改屬性的值時,KVO會自動調用以下兩個方法:
- (void)willChangeValueForKey:(NSString *)key- (void)didChangeValueForKey:(NSString *)key
這兩個方法的任務是告訴接收者給定的屬性將要或已經被修改。需要注意的是不應該在子類中去重寫這兩個方法。
但如果我們希望自己控制通知發送的一些細節,則可以啟用手動控制模式。手動控制通知提供了對KVO更精確控制,它可以控制通知如何以及何時被發送給觀察者。采用這種方式可以減少不必要的通知,或者可以將多個修改組合到一個修改中。
實現手動通知的類必須重寫NSObject中對automaticallyNotifiesObserversForKey:方法的實現。這個方法是在NSKeyValueObserving協議中聲明的,其聲明如下:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
這個方法返回一個布爾值(默認是返回YES),以標識參數key指定的屬性是否支持自動KVO。如果我們希望手動去發送通知,則針對指定的屬性返回NO。
假設我們希望PersonObject對象去監聽BankObject對象的bankCodeEn屬性,并希望執行手動通知,則可以如下處理:
代碼清單4:關閉屬性的自動通知發送
@implementation BankObject+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { BOOL automatic = YES; if ([key isEqualToString:@"bankCodeEn"]) { automatic = NO; } else { automatic = [super automaticallyNotifiesObserversForKey:key]; } return automatic;}@end
這樣我們便可以手動去發送屬性修改通知了。需要注意的是,對于對象中其它沒有處理的屬性,我們需要調用[super automaticallyNotifiesObserversForKey:key],以避免無意中修改了父類的屬性的處理方式。
現在我們已經通過+automaticallyNotifiesObserversForKey:方法設置了對象中哪些屬性需要手動處理。接下來就是實際操作了。為了實現手動發送通知,我們需要在修改屬性值前調用willChangeValueForKey:,然后在修改屬性值之后調用didChangeValueForKey:方法。繼續上面的示例,我們需要對bankCodeEn屬性做如下處理:
代碼清單5:手動控制通知發送
@implementation BankObject- (void)setBankCodeEn:(NSString *)bankCodeEn { [self willChangeValueForKey:@"bankCodeEn"]; _bankCodeEn = bankCodeEn; [self didChangeValueForKey:@"bankCodeEn"];}@end
如果我們希望只有當bankCodeEn實際被修改時發送通知,以盡量減少不必要的通知,則可以如下實現:
代碼清單6:在發送通知前測試值是否修改
- (void)setBankCodeEn:(NSString *)bankCodeEn { if (bankCodeEn != _bankCodeEn) { [self willChangeValueForKey:@"bankCodeEn"]; _bankCodeEn = bankCodeEn; [self didChangeValueForKey:@"bankCodeEn"]; }}
我們來測試一下上面這段代碼的實際效果:
代碼清單7:測試避免屬性未實際修改下不發送通知
PersonObject *personInstance = [[PersonObject alloc] init];BankObject *bankInstance = [[BankObject alloc] init];[bankInstance addObserver:personInstance forKeyPath:@"bankCodeEn" options:NSKeyValueObservingOptionNew context:NULL];NSString *bankCodeEn = @"CCB";bankInstance.bankCodeEn = bankCodeEn;bankInstance.bankCodeEn = bankCodeEn;
這段代碼的輸出結果如下所示:
keyPath = bankCodeEn, change = { kind = 1; new = CCB;}, context = (null)
我們可以看到只輸出了一次,而不是兩次。
如果我們在setter方法之外改變了實例變量(如_bankCodeEn),且希望這種修改被觀察者監聽到,則需要像在setter方法里面做一樣的處理。這也涉及到我們通常會遇到的一個問題,在類的內部,對于一個屬性值,何時用屬性(self.bankCodeEn)訪問而何時用實例變量(_bankCodeEn)訪問。一般的建議是,在獲取屬性值時,可以用實例變量,在設置屬性值時,盡量用setter方法,以保證屬性的KVO特性。當然,性能也是一個考量,在設置值時,使用實例變量比使用屬性設置值的性能高不少。
另外,對于一對多關系的屬性,如果想手動處理通知,則可以使用以下幾個方法:
// 有序的一對多關系- (void)willChange:(NSKeyValueChange)change valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key- (void)didChange:(NSKeyValueChange)change valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key// 無序的一對多關系- (void)willChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects- (void)didChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects
同樣,在子類中也不應該去重寫這幾個方法。
計算屬性(注冊依賴鍵)
有時候,我們的監聽的某個屬性可能會依賴于其它多個屬性的變化(類似于swift,可以稱之為計算屬性),不管所依賴的哪個屬性發生了變化,都會導致計算屬性的變化。對于這種一對一(To-one)的關系,我們需要做兩步操作,首先是確定計算屬性與所依賴屬性的關系。如我們在BankObject類中定義一個accountForBank屬性,其get方法定義如下:
代碼清單8:計算屬性
@implementation BankObject- (NSString *)accountForBank { return [NSString stringWithFormat:@"account for %@ is %d", self.bankCodeEn, self.accountBalance];}@end
定義了這種依賴關系后,我們就需要以某種方式告訴KVO,當我們的被依賴屬性修改時,會發送accountForBank屬性被修改的通知。此時,我們需要重寫NSKeyValueObserving協議的keyPathsForValuesAffectingValueForKey:方法,該方法聲明如下:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
這個方法返回的是一個集合對象,包含了那些影響key指定的屬性依賴的屬性所對應的字符串。所以對于accountForBank屬性,該方法的實現如下:
代碼清單9:accountForBank屬性的keyPathsForValuesAffectingValueForKey方法的實現
@implementation BankObject+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqualToString:@"accountForBank"]) { keyPaths = [keyPaths setByAddingObjectsFromArray:@[@"accountBalance", @"bankCodeEn"]]; } return keyPaths;}@end
我們來再來看看監聽accountForBank屬性是什么效果:
代碼清單10:監聽accountForBank屬性
PersonObject *personInstance = [[PersonObject alloc] init];BankObject *bankInstance = [[BankObject alloc] init];[bankInstance addObserver:personInstance forKeyPath:@"accountForBank" options:NSKeyValueObservingOptionNew context:NULL];bankInstance.accountBalance = 10;bankInstance.bankCodeEn = @"CCB";
其輸出結果為:
keyPath = accountForBank, change = { kind = 1; new = "account for (null) is 10";}, context = (null)keyPath = accountForBank, change = { kind = 1; new = "account for CCB is 10";}, context = (null)
可以看到,不管是accountBalance還是bankCodeEn被修改了,都會發送accountForBank屬性被修改的通知。
需要注意的就是當我們重寫+keyPathsForValuesAffectingValueForKey:時,需要去調用super的對應方法,并返回一個包含父類中可能會對key指定屬性產生影響的屬性集合。
另外,我們還可以實現一個命名為keyPathsForValuesAffecting<Key>的類方法來達到同樣的目的,其中是我們計算屬性的名稱。所以對于accountForBank屬性,還可以如下實現:
+ (NSSet *)keyPathsForValuesAffectingAccountForBank { return [NSSet setWithObjects:@"accountBalance", @"bankCodeEn", nil];}
兩種方法的實現效果是一樣的。不過更建議使用后面一種方法,這種方法讓依賴關系更加清晰明了。
集合屬性的監聽
keyPathsForValuesAffectingValueForKey:只支持一對一的關系,而不支持一對多的關系,即不支持對集合的處理。
對于集合的KVO,我們需要了解的一點是,KVO旨在觀察關系(relationship)而不是集合。對于不可變集合屬性,我們更多的是把它當成一個整體來監聽,而無法去監聽集合中的某個元素的變化;對于可變集合屬性,實際上也是當成一個整體,去監聽它整體的變化,如添加、刪除和替換元素。
在KVC中,我們可以使用集合代理對象(collection proxy object)來處理集合相關的操作。我們以數組為例,在我們的BankObject類中有一個departments數組屬性,如果我們希望通過集合代理對象來負責響應departments所有的方法,則需要實現以下方法:
-countOf<Key>// 以下兩者二選一-objectIn<Key>AtIndex:-<key>AtIndexes:// 可選(增強性能)-get<Key>:range:
因此,我們的實現以下幾個方法:
代碼清單11:集合代碼對象的實現
@implementation BankObject#pragma mark - 集合代理對象- (NSUInteger)countOfDepartments { return [_departments count];}- (id)objectInDepartmentsAtIndex:(NSUInteger)index { return [_departments objectAtIndex:index];}@end
實現以上方法之后,對于不可變數組,當我們調用[bankInstance valueForKey:@“departments”]的時候,便會返回一個由以上方法來代理所有調用方法的NSArray對象。這個代理數組對象支持所有正常的NSArray調用。換句話說,調用者并不知道返回的是一個真正的NSArray,還是一個代理的數組。
另外,對于可變數組的代理對象,我們需要實現以下幾個方法:
// 至少實現一個插入方法和一個刪除方法-insertObject:in<Key>AtIndex:-removeObjectFrom<Key>AtIndex:-insert<Key>:atIndexes:-remove<Key>AtIndexes:// 可選(增強性能)以下方法二選一-replaceObjectIn<Key>AtIndex:withObject:-replace<Key>AtIndexes:with<Key>:
這些方法分別對應插入、刪除和替換,有批量操作的,也有只改變一個對象的方法??梢愿鶕嶋H需要來實現。
另外,對于可變集合,我們通常不使用valueForKey:來獲取代理對象,而是使用以下方法:
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
通過這個方法,我們便可以將可變數組與強大的KVO結合在一起。KVO機制能在集合改變的時候把詳細的變化放進change字典中。
我們先來看看下面的代碼:
代碼清單12:使用真正的數組對象監聽可變數組屬性
BankObject *bankInstance = [[BankObject alloc] init];PersonObject *personInstance = [[PersonObject alloc] init];[bankInstance addObserver:personInstance forKeyPath:@"departments" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];bankInstance.departments = [[NSMutableArray alloc] init];[bankInstance.departments addObject:@"departments"];
其輸出為:
keyPath = departments, change = { kind = 1; new = ( ); old = ( );}, context = (null)
可以看到通過這種方法,我們獲取的是真正的數組,只在departments屬性整體被修改時,才會觸發KVO,而在添加元素時,并沒有觸發KVO。
現在我們通過代理集合對象來看看:
代碼清單13:使用代理集合對象監聽可變數組屬性
BankObject *bankInstance = [[BankObject alloc] init];PersonObject *personInstance = [[PersonObject alloc] init];[bankInstance addObserver:personInstance forKeyPath:@"departments" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];bankInstance.departments = [[NSMutableArray alloc] init];NSMutableArray *departments = [bankInstance mutableArrayValueForKey:@"departments"];[departments insertObject:@"departments 0" atIndex:0];
其輸出是:
keyPath = departments, change = { kind = 1; new = ( ); old = ( );}, context = (null)keyPath = departments, change = { indexes = "<NSIndexSet: 0x7fcd18673150>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 2; new = ( "departments 0" );}, context = (null)
可以看到,在往數組中添加對象時,也觸發了KVO,并將改變的詳細信息也寫進了change字典。在第二個消息中,kind的值為2,即表示這是一次插入操作。同樣,可變數組的刪除,替換操作也是一樣的。
集合(Set)也有一套對應的方法來實現集合代理對象,包括無序集合與有序集合;而字典則沒有,對于字典屬性的監聽,還是只能作為一個整理來處理。
如果我們想到手動控制集合屬性消息的發送,則可以使用上面提到的幾個方法,即:
-willChange:valuesAtIndexes:forKey:-didChange:valuesAtIndexes:forKey:或-willChangeValueForKey:withSetMutation:usingObjects:-didChangeValueForKey:withSetMutation:usingObjects:
不過得先保證把自動通知關閉,否則每次改變KVO都會被發送兩次。
監聽信息
如果我們想獲取一個對象上有哪些觀察者正在監聽其屬性的修改,則可以查看對象的observationInfo屬性,其聲明如下:
@property void *observationInfo
可以看到它是一個void指針,指向一個包含所有觀察者的一個標識信息對象,這些信息包含了每個監聽的觀察者,注冊時設定的選項等等。我們還是用示例來看看。
代碼清單14:observationInfo的使用
PersonObject *personInstance = [[PersonObject alloc] init];BankObject *bankInstance = [[BankObject alloc] init];[bankInstance addObserver:personInstance forKeyPath:@"bankCodeEn" options:NSKeyValueObservingOptionNew context:NULL];[bankInstance addObserver:personInstance forKeyPath:@"accountBalance" options:NSKeyValueObservingOptionOld context:NULL];NSLog(@"%p", personInstance);NSLog(@"%p", bankInstance);id info = bankInstance.observationInfo;NSLog(@"%@", [info description]);
其輸出結果如下:
personInstance = 0x7fdc2369e5e0bankInstance = 0x7fdc2369d8f0<NSKeyValueObservationInfo 0x7fdc236a19d0> (<NSKeyValueObservance 0x7fdc236a17a0: Observer: 0x7fdc2369e5e0, Key path: bankCodeEn, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x7fdc236a15c0><NSKeyValueObservance 0x7fdc236a1960: Observer: 0x7fdc2369e5e0, Key path: accountBalance, Options: <New: NO, Old: YES, Prior: NO> Context: 0x0, Property: 0x7fdc236a1880>)
我們可以看到observationInfo指針實際上是指向一個NSKeyValueObservationInfo對象,它包含了指定對象上的所有的監聽信息。而每條監聽信息而是封裝在一個NSKeyValueObservance對象中,從上面可以看到,這個對象中包含消息的觀察者、被監聽的屬性、添加觀察者時所設置的一些選項、上下文信息等。
NSKeyValueObservationInfo類及NSKeyValueObservance類都是私有類,我們無法在官方文檔中找到這兩個類的實現。不過從一些對系統庫dump出來的頭文件,我們可以對這兩個類有一些初步的了解。這里有一個對iOS SKD 4.3的Foundation.framework的dump頭文件,可以找到這兩個類的頭文件,其中NSKeyValueObservationInfo的頭文件信息如下所示:
#import <XXUnknownSuperclass.h> // Unknown library@class NSArray, NSHashTable;__attribute__((visibility("hidden")))@interface NSKeyValueObservationInfo : XXUnknownSuperclass {@private int _retainCountMinusOne; NSArray* _observances; unsigned _cachedHash; BOOL _cachedIsShareable; NSHashTable* _observables;}-(id)_initWithObservances:(id*)observances count:(unsigned)count;-(id)retain;-(oneway void)release;-(unsigned)retainCount;-(void)dealloc;-(unsigned)hash;-(BOOL)isEqual:(id)equal;-(id)description;@end
可以看到其中有一個數組來存儲NSKeyValueObservance對象。
NSKeyValueObservance類的頭文件信息如下:
#import "Foundation-Structs.h"#import <XXUnknownSuperclass.h> // Unknown library@class NSPointerArray, NSKeyValueProperty, NSObject;__attribute__((visibility("hidden")))@interface NSKeyValueObservance : XXUnknownSuperclass {@private int _retainCountMinusOne; NSObject* _observer; NSKeyValueProperty* _property; unsigned _options; void* _context; NSObject* _originalObservable; unsigned _cachedUnrotatedHashComponent; BOOL _cachedIsShareable; NSPointerArray* _observationInfos; auto_weak_callback_block _observerWentAwayCallback;}-(id)_initWithObserver:(id)observer property:(id)property options:(unsigned)options context:(void*)context originalObservable:(id)observable;-(id)retain;-(oneway void)release;-(unsigned)retainCount;-(void)dealloc;-(unsigned)hash;-(BOOL)isEqual:(id)equal;-(id)description;-(void)observeValueForKeyPath:(id)keyPath ofObject:(id)object change:(id)change context:(void*)context;@end
可以看到其中包含了一個監聽的基本要素。在此不再做深入分析(沒有源代碼,深入不下去了啊)。
我們再回到observationInfo屬性本身來。在文檔中,對這個屬性的描述有這樣一段話:
The default implementation of this method retrieves the information from a globaldictionary keyed by the receiver’s pointers.
即這個方法的默認實現是以對象的指針作為key,從一個全局的字典中獲取信息。由此,我們可以理解為,KVO的信息是存儲在一個全局字典中,而不是存儲在對象本身。這類似于Notification,所有關于通知的信息都是放在NSNotificationCenter中。
不過,為了提高效率,我們可以重寫observationInfo屬性的set和get方法,以將這個不透明的數據指針存儲到一個實例變量中。但是,在重寫時,我們不應該嘗試去向這些數據發送一個Objective-C消息,包括retain和release。
KVO的實現機制
【本來這一小節是想放在另一篇總結中來寫的,但后來覺得還是放在這里比較合適,所以就此添加上】
了解了NSKeyValueObserving所提供的功能后,我們再來看看KVO的實現機制,以便更深入地的理解KVO。
KVO據我所查還沒有開源(若哪位大大有查到源代碼,還煩請告知),所以我們無法從源代碼的層面來分析它的實現。不過Mike Ash的博文(譯文見參考文獻4)為我們解開了一些謎團。
基本的思路是:Objective-C依托于強大的run time機制來實現KVO。當我們第一次觀察某個對象的屬性時,run time會創建一個新的繼承自這個對象的class的subclass。在這個新的subclass中,它會重寫所有被觀察的key的setter,然后將object的isa指針指向新創建的class(這個指針告訴Objective-C運行時某個object到底是什么類型的)。所以object神奇地變成了新的子類的實例。
嗯,讓我們通過代碼來看看實際的實現:
代碼清單15:探究KVO的實現機制
// 輔助方法static NSArray *ClassMethodNames(Class c) { NSMutableArray *array = [NSMutableArray array]; unsigned int methodCount = 0; Method *methodList = class_copyMethodList(c, &methodCount); unsigned int i; for (i = 0; i < methodCount; i++) { [array addObject:NSStringFromSelector(method_getName(methodList[i]))]; } free(methodList); return array;}static void PrintDescription(NSString *name, id obj) { struct objc_object *objcet = (__bridge struct objc_object *)obj; Class cls = objcet->isa; NSString *str = [NSString stringWithFormat:@"%@: %@/n/tNSObject class %s/n/tlibobjc class %s : super class %s/n/timplements methods <%@>", name, obj, class_getName([obj class]), class_getName(cls), class_getName(class_getSuperclass(cls)), [ClassMethodNames(cls) componentsJoinedByString:@", "]]; printf("%s/n", [str UTF8String]);}// 測試代碼BankObject *bankInstance1 = [[BankObject alloc] init];BankObject *bankInstance2 = [[BankObject alloc] init];PersonObject *personInstance = [[PersonObject alloc] init];[bankInstance2 addObserver:personInstance forKeyPath:@"accountBalance" options:NSKeyValueObservingOptionNew context:NULL];PrintDescription(@"bankInstance1", bankInstance1);PrintDescription(@"bankInstance2", bankInstance2);printf("Using libobjc functions, normal setAccountBalance: is %p, overridden setAccountBalance: is %p", method_getImplementation(class_getInstanceMethod(object_getClass(bankInstance2), @selector(setAccountBalance:))), method_getImplementation(class_getInstanceMethod(object_getClass(bankInstance1), @selector(setAccountBalance:))));
這段代碼的輸出如下:
bankInstance1: <BankObject: 0x7f9e8ae3cf60> NSObject class BankObject libobjc class BankObject : super class NSObject implements methods <accountBalance, setAccountBalance:, bankCodeEn, setBankCodeEn:, departments, setDepartments:>bankInstance2: <BankObject: 0x7f9e8ae3cfc0> NSObject class BankObject libobjc class NSKVONotifying_BankObject : super class BankObject implements methods <setAccountBalance:, class, dealloc, _isKVOA>Using libobjc functions, normal setAccountBalance: is 0x1013cec17, overridden setAccountBalance: is 0x10129fe50
從輸出中可以看到,bankInstance2監聽accountBalance屬性后,其實際上所屬的類已經不是BankObject了,而是繼承自BankObject的NSKVONotifying_BankObject類。同時,NSKVONotifying_BankObject類重寫了setAccountBalance方法,這個方法將實現如何通知觀察者們的操作。當改變accountBalance屬性時,就會調用被重寫的setAccountBalance方法,并通過這個方法來發送通知。
另外我們也可以看到bankInstance2對象的打印[bankInstance2 class]時,返回的仍然是BankObject。這是蘋果故意而為之,他們不希望這個機制暴露在外面。所以除了重寫相應的setter,所以動態生成的NSKVONotifying_BankObject類還重寫了class方法,讓它返回原先的類。
小結
KVO作為Objective-C中兩個對象間通信機制中的一種,提供了一種非常強大的機制。在經典的MVC架構中,控制器需要確保視圖與模型的同步,當model對象改變時,視圖應該隨之改變以反映模型的變化;當用戶和控制器交互的時候,模型也應該做出相應的改變。而KVO便為我們提供了這樣一種同步機制:我們讓控制器去監聽一個model對象屬性的改變,并根據這種改變來更新我們的視圖。所有,有效地使用KVO,對我們應用的開發意義重大。
別話:對KVO的總結感覺還是意猶未盡,總感覺缺少點什么,特別是在對集合這一塊的處理。還請大家多多提供指點。