KVC/KVO原理詳解及程式設計指南(轉載)

斯人如是丶發表於2016-05-05
作者:wangzz

前言:
1、本文基本不講KVC/KVO的用法,只結合網上的資料說說對這種技術的理解。
2、由於KVO內容較少,而且是以KVC為基礎實現的,本文將著重介紹KVC部分。

一、簡介

KVC/KVO是觀察者模式的一種實現,在Cocoa中是以被萬物之源NSObject類實現的NSKeyValueCoding/NSKeyValueObserving非正式協議的形式被定義為基礎框架的一部分。從協議的角度來說,KVC/KVO本質上是定義了一套讓我們去遵守和實現的方法。
當然,KVC/KVO實現的根本是Objective-C的動態性和runtime,這在後文的原理部分會有詳述。
另外,KVC/KVO機制離不開訪問器方法的實現,這在後文中也有解釋。

1、KVC簡介

全稱是Key-value coding,翻譯成鍵值編碼。顧名思義,在某種程度上跟map的關係匪淺。它提供了一種使用字串而不是訪問器方法去訪問一個物件例項變數的機制。

2、KVO簡介

全稱是Key-value observing,翻譯成鍵值觀察。提供了一種當其它物件屬性被修改的時候能通知當前物件的機制。再MVC大行其道的Cocoa中,KVO機制很適合實現model和controller類之間的通訊。

二、KVC相關技術

1、Key和Key Path

KVC定義了一種按名稱訪問物件屬性的機制,支援這種訪問的主要方法是:
[java] view plain copy
  1. - (id)valueForKey:(NSString *)key;  
  2. - (void)setValue:(id)value forKey:(NSString *)key;  
  3. - (id)valueForKeyPath:(NSString *)keyPath;  
  4. - (void)setValue:(id)value forKeyPath:(NSString *)keyPath;  
前邊兩個方法用到的Key較容易理解,就是要訪問的屬性名稱對應的字串。
後面兩個方法用到的KeyPath是一個被點操作符隔開的用於訪問物件的指定屬性的字串序列。比如KeyPath address.street將會訪問訊息接收物件所包含的address屬性中包含的一個street屬性。其實KeyPath說白了就是我們平時使用點操作訪問某個物件的屬性時所寫的那個字串。

2、點語法和KVC

在實現了訪問器方法的類中,使用點語法和KVC訪問物件其實差別不大,二者可以任意混用。但是沒有訪問起方法的類中,點語法無法使用,這時KVC就有優勢了。(原因見第三部分的第一節:KVC如何訪問屬性值。)

3、一對多關係(To-Many)中的集合訪問器方法

我們平時大部分使用的屬性都是一對一關係(To-One),比如Person類中的name屬性,每個人只有一個名字。但也有一對多的關係,比如Person中有一個friendsName屬性,這是個集合(在Objective-C中可以是NSArray,NSSet等),儲存的是一個人的所有朋友的名字。
當操作一對多的屬性中的內容時,我們有兩種選擇:
①間接操作
先通過KVC方法取到集合屬性,然後通過集合屬性操作集合中的元素。
②直接操作
蘋果為我們提供了一些方法模板,我們可以以規定的格式實現這些方法來達到訪問集合屬性中元素的目的。
有序集合對應方法如下:
[java] view plain copy
  1. -countOf<Key>  
  2. //必須實現,對應於NSArray的基本方法count:  
  3. -objectIn<Key>AtIndex:  
  4. -<key>AtIndexes:  
  5. //這兩個必須實現一個,對應於 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:  
  6. -get<Key>:range:  
  7. //不是必須實現的,但實現後可以提高效能,其對應於 NSArray 方法 getObjects:range:  
  8.   
  9. -insertObject:in<Key>AtIndex:  
  10. -insert<Key>:atIndexes:  
  11. //兩個必須實現一個,類似於 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:  
  12. -removeObjectFrom<Key>AtIndex:  
  13. -remove<Key>AtIndexes:  
  14. //兩個必須實現一個,類似於 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:  
  15. -replaceObjectIn<Key>AtIndex:withObject:  
  16. -replace<Key>AtIndexes:with<Key>:  
  17. //可選的,如果在此類操作上有效能問題,就需要考慮實現之  
無序集合對應方法如下:
[java] view plain copy
  1. -countOf<Key>  
  2. //必須實現,對應於NSArray的基本方法count:  
  3. -objectIn<Key>AtIndex:  
  4. -<key>AtIndexes:  
  5. //這兩個必須實現一個,對應於 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:  
  6. -get<Key>:range:  
  7. //不是必須實現的,但實現後可以提高效能,其對應於 NSArray 方法 getObjects:range:  
  8.   
  9. -insertObject:in<Key>AtIndex:  
  10. -insert<Key>:atIndexes:  
  11. //兩個必須實現一個,類似於 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:  
  12. -removeObjectFrom<Key>AtIndex:  
  13. -remove<Key>AtIndexes:  
  14. //兩個必須實現一個,類似於 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:  
  15. -replaceObjectIn<Key>AtIndex:withObject:  
  16. -replace<Key>AtIndexes:with<Key>:  
  17. //這兩個都是可選的,如果在此類操作上有效能問題,就需要考慮實現之  
不過這些方法除非是很有需求,否則個人認為沒有實現的必要,間接法也不是很麻煩,基本能滿足需求了。值得指出的是,蘋果甚至都沒有讓這些方法以哪怕是非正式協議的形式出現,而只是在程式設計指南中提了一下。

4、鍵值驗證(Key-Value Validation)

KVC提供了驗證Key對應的Value是否可用的方法:
[java] view plain copy
  1. - (BOOL)validateValue:(inout id *)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;  
該方法預設的實現是呼叫一個如下格式的方法:
[java] view plain copy
  1. - (BOOL)validate<Key>:error:  
比如屬性name對應的方法為:
[java] view plain copy
  1. -(BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError {  
  2.     // Implementation specific code.  
  3.     return ...;  
  4. }  
這樣就給了我們一次糾錯的機會。
需要指出的是,KVC是不會自動呼叫鍵值驗證方法的,就是說我們需要手動驗證。但是有些技術,比如CoreData會自動呼叫。

5、KVC對數值和結構體型屬性的支援

一套機制如果不支援數值和結構體型的資料,那麼它的實用性就會大大折扣。幸運的是KVC中蘋果對這方面的支援做的很好。KVC可以自動的將數值或結構體型的資料打包或解包成NSNumber或NSValue物件,以達到適配的目的。
舉個例子,Person類有個個NSInteger型別的age屬性
①修改值
我們通過KVC技術使用如下方式設定age屬性的值:
[java] view plain copy
  1. [person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];  
我們賦給age的是一個NSNumber物件,KVC會自動的將NSNumber物件轉換成NSInteger物件,然後再呼叫相應的訪問器方法設定age的值。
②獲取值
同樣,以如下方式獲取age屬性值:
[java] view plain copy
  1. [person valueForKey:@"age"];  
這時,會以NSNumber的形式返回age的值。
需要說明的是,什麼時候返回的是NSNumber,什麼時候返回的是NSValue?
③使用NSNumber封裝
可以使用NSNumber的資料型別有:
[java] view plain copy
  1. + (NSNumber *)numberWithChar:(char)value;  
  2. + (NSNumber *)numberWithUnsignedChar:(unsigned char)value;  
  3. + (NSNumber *)numberWithShort:(short)value;  
  4. + (NSNumber *)numberWithUnsignedShort:(unsigned short)value;  
  5. + (NSNumber *)numberWithInt:(int)value;  
  6. + (NSNumber *)numberWithUnsignedInt:(unsigned int)value;  
  7. + (NSNumber *)numberWithLong:(long)value;  
  8. + (NSNumber *)numberWithUnsignedLong:(unsigned long)value;  
  9. + (NSNumber *)numberWithLongLong:(long long)value;  
  10. + (NSNumber *)numberWithUnsignedLongLong:(unsigned long long)value;  
  11. + (NSNumber *)numberWithFloat:(float)value;  
  12. + (NSNumber *)numberWithDouble:(double)value;  
  13. + (NSNumber *)numberWithBool:(BOOL)value;  
  14. + (NSNumber *)numberWithInteger:(NSInteger)value NS_AVAILABLE(10_5, 2_0);  
  15. + (NSNumber *)numberWithUnsignedInteger:(NSUInteger)value NS_AVAILABLE(10_5, 2_0);  
總之就是一些常見的數值型資料。
④使用NSValue封裝
NSValue主要用於處理結構體型的資料,它本身提供瞭如下集中結構的支援:
[java] view plain copy
  1. + (NSValue *)valueWithCGPoint:(CGPoint)point;  
  2. + (NSValue *)valueWithCGSize:(CGSize)size;  
  3. + (NSValue *)valueWithCGRect:(CGRect)rect;  
  4. + (NSValue *)valueWithCGAffineTransform:(CGAffineTransform)transform;  
  5. + (NSValue *)valueWithUIEdgeInsets:(UIEdgeInsets)insets;  
  6. + (NSValue *)valueWithUIOffset:(UIOffset)insets NS_AVAILABLE_IOS(5_0);  
只有有限的6種而已!那對於其它自定義的結構體怎麼辦?別擔心,任何結構體都是可以轉化成NSValue物件的,具體實現方法參見我之前的一篇文章:

6、集合運算子(Collection Operators)

集合運算子是一個特殊的Key Path,可以作為引數傳遞給valueForKeyPath:方法,注意只能是這個方法,如果傳給了valueForKey:方法保證你程式崩潰。
運算子是一個以@開頭的特殊字串,格式如下圖所示:

①簡單集合運算子
簡單集合運算子共有@avg,@count,@max,@min,@sum5種,都表示啥不用我說了吧,目前還不支援自定義
有一個集合類的物件:transactions,它儲存了一個個的Transaction類的例項,該類有三個屬性:payee,amount,date。下面以此為例說明如何使用這些運算子:
要獲取amount的平均值可以這樣:
[java] view plain copy
  1. NSNumber *transactionAverage = [transactions valueForKeyPath:@"@avg.amount"];  
要獲取transactions集合中元素數目可以這樣:
[java] view plain copy
  1. NSNumber *numberOfTransactions = [transactions valueForKeyPath:@"@count"];  
需要之處的是,@count是這些集合運算子中比較特殊的一個,因為它沒有右路經,原因很容易理解。
②物件運算子
比集合運算子稍微複雜,能以陣列的方式返回指定的內容,一共有兩種:
[java] view plain copy
  1. @distinctUnionOfObjects  
  2. @unionOfObjects  
它們的返回值都是NSArray,區別是前者返回的元素都是唯一的,是去重以後的結果;後者返回的元素是全集。
用法如下:
[java] view plain copy
  1. NSArray *payees = [transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];  
  2. NSArray *payees = [transactions valueForKeyPath:@"@unionOfObjects.payee"];  
前者會將收款人的姓名去除重複的以後返回,後者直接返回所有收款人的姓名。
③Array和Set操作符
這種情況更復雜了,說的是集合中包含集合的情況,我們執行了如下的一段程式碼:
[java] view plain copy
  1. // Create the array that contains additional arrays.  
  2. self.arrayOfTransactionsArray = [NSMutableArray array];  
  3.    
  4. // Add the array of objects used in the above examples.  
  5. [arrayOfTransactionsArray addObject:transactions];  
  6.    
  7. // Add a second array of objects; this array contains alternate values.  
  8. [arrayOfTransactionsArrays addObject:moreTransactions];  
得到了一個包含集合的集合:arrayOfTransactionsArray
這時如果我們想操作arrayOfTransactionsArray中包含的集合中的元素時,可以使用如下三個運算子:
[java] view plain copy
  1. @distinctUnionOfArrays  
  2. @unionOfArrays  
  3. @distinctUnionOfSets  
前兩個針對的集合是Arrays,後一個針對的集合是Sets。因為Sets中的元素本身就是唯一的,所以沒有對應的@unionOfSets運算子。
它們的用法舉例如下:
[java] view plain copy
  1. NSArray *payees = [arrayOfTransactionsArrays valueForKeyPath:@"@unionOfArrays.payee"];  

三、實現原理

1、KVC如何訪問屬性值

KVC再某種程度上提供了訪問器的替代方案。不過訪問器方法是一個很好的東西,以至於只要是有可能,KVC也儘量再訪問器方法的幫助下工作。為了設定或者返回物件屬性,KVC按順序使用如下技術:
①檢查是否存在-<key>、-is<key>(只針對布林值有效)或者-get<key>的訪問器方法,如果有可能,就是用這些方法返回值;
檢查是否存在名為-set<key>:的方法,並使用它做設定值。對於-get<key>和-set<key>:方法,將大寫Key字串的第一個字母,並與Cocoa的方法命名保持一致;
②如果上述方法不可用,則檢查名為-_<key>、-_is<key>(只針對布林值有效)、-_get<key>和-_set<key>:方法;
③如果沒有找到訪問器方法,可以嘗試直接訪問例項變數。例項變數可以是名為:<key>或_<key>;
④如果仍為找到,則呼叫valueForUndefinedKey:和setValue:forUndefinedKey:方法。這些方法的預設實現都是丟擲異常,我們可以根據需要重寫它們。

2、KVC/KVO實現原理

鍵值編碼和鍵值觀察是根據isa-swizzling技術來實現的,主要依據runtime的強大動態能力。下面的這段話是引自網上的一篇文章:
當某個類的物件第一次被觀察時,系統就會在執行期動態地建立該類的一個派生類,在這個派生類中重寫基類中任何被觀察屬性的 setter 方法。
派生類在被重寫的 setter 方法實現真正的通知機制
,就如前面手動實現鍵值觀察那樣。這麼做是基於設定屬性會呼叫 setter 方法,而通過重寫就獲得了 KVO 需要的通知機制。當然前提是要通過遵循 KVO 的屬性設定方式來變更屬性值,如果僅是直接修改屬性對應的成員變數,是無法實現 KVO 的。
同時派生類還重寫了 class 方法以“欺騙”外部呼叫者它就是起初的那個類。然後系統將這個物件的 isa 指標指向這個新誕生的派生類,因此這個物件就成為該派生類的物件了,因而在該物件上對 setter 的呼叫就會呼叫重寫的 setter,從而啟用鍵值通知機制。此外,派生類還重寫了 dealloc 方法來釋放資源。
原文寫的很好,還舉了解釋性的例子,大家可以去看看。
在我之前的一篇介紹Objective-C類和元類的文章:
中介紹過,isa指標指向的其實是類的元類,如果之前的類名為:Person,那麼被runtime更改以後的類名會變成:NSKVONotifying_Person。
新的NSKVONotifying_Person類會重寫以下方法:
增加了監聽的屬性對應的set方法,class,dealloc,_isKVOA。
①class
重寫class方法是為了我們呼叫它的時候返回跟重寫繼承類之前同樣的內容。
列印如下內容:
[java] view plain copy
  1. NSLog(@"self->isa:%@",self->isa);  
  2. NSLog(@"self class:%@",[self class]);  
在建立KVO監聽前,列印結果為:
[java] view plain copy
  1. self->isa:Person  
  2. self class:Person  
在建立KVO監聽之後,列印結果為:
[java] view plain copy
  1. self->isa:NSKVONotifying_Person  
  2. self class:Person  
這也是isa指標和class方法的一個區別,大家使用的時候注意。
②重寫set方法
新類會重寫對應的set方法,是為了在set方法中增加另外兩個方法的呼叫:
[java] view plain copy
  1. - (void)willChangeValueForKey:(NSString *)key  
  2. - (void)didChangeValueForKey:(NSString *)key  
其中,didChangeValueForKey:方法負責呼叫:
[java] view plain copy
  1. - (void)observeValueForKeyPath:(NSString *)keyPath  
  2.                       ofObject:(id)object  
  3.                         change:(NSDictionary *)change  
  4.                        context:(void *)context  
方法,這就是KVO實現的原理了!
如果沒有任何的訪問器方法,-setValue:forKey方法會直接呼叫:
[java] view plain copy
  1. - (void)willChangeValueForKey:(NSString *)key  
  2. - (void)didChangeValueForKey:(NSString *)key  
如果在沒有使用鍵值編碼且沒有使用適當命名的訪問起方法的時候,我們只需要顯示呼叫上述兩個方法,同樣可以使用KVO!
總結一下,想使用KVO有三種方法:
1)使用了KVC
使用了KVC,如果有訪問器方法,則執行時會在訪問器方法中呼叫will/didChangeValueForKey:方法;
沒用訪問器方法,執行時會在setValue:forKey方法中呼叫will/didChangeValueForKey:方法。
2)有訪問器方法
執行時會重寫訪問器方法呼叫will/didChangeValueForKey:方法。
因此,直接呼叫訪問器方法改變屬性值時,KVO也能監聽到。
3)顯示呼叫will/didChangeValueForKey:方法。
總之,想使用KVO,只要有will/didChangeValueForKey:方法就可以了。
③_isKVOA
這個私有方法估計是用來標示該類是一個 KVO 機制聲稱的類。

四、優點和缺點

1、優點

①可以再很大程度上簡化程式碼
例子網上很多,這就不舉了
②能跟指令碼語言很好的配合
才疏學淺,沒學過AppleScript等指令碼語言,所以沒能深刻體會到該優點。

2、缺點

KVC的缺點不明顯,主要是KVO的,詳情可以參考這篇文章:
核心思想是說KVO的回撥機制,不能傳一個selector或者block作為回撥,而必須重寫-addObserver:forKeyPath:options:context:方法所引發的一系列問題。問了解決這個問題,作者還親自實現了一個MAKVONotificationCenter類,程式碼見github:
不過個人認為這只是蘋果做的KVO不夠完美,不能算是缺陷。

參考文件:

相關文章