天天看點

ios-kvc\kvo 原理

ios-kvc\kvo 原理

原文位址:http://blog.csdn.net/wzzvictory/article/details/9674431

KVC(Key-value coding)鍵值編碼,類似于map,提供了一種使用字元串而不是通路器方法去通路一個對象執行個體變量的機制。         

KVO(Key-value observing)鍵值觀察,提供了一種當其它對象屬性被修改的時候能通知目前對象的機制。

1、Key和Key Path

KVC定義了一種按名稱通路對象屬性的機制,支援這種通路的主要方法是:

  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;

2、點文法和KVC

在實作了通路器方法的類中,使用點文法和KVC通路對象其實差别不大,二者可以任意混用。但是沒有通路器方法的類中,點文法無法使用,這時KVC就有優勢了。

3、一對多關系(To-Many)中的集合通路器方法

我們平時大部分使用的屬性都是一對一關系(To-One),比如Person類中的name屬性,每個人隻有一個名字。但也有一對多的關系,比如Person中有一個friendsName屬性,這是個集合(在Objective-C中可以是NSArray,NSSet等),儲存的是一個人的所有朋友的名字。 當操作一對多的屬性中的内容時,我們有兩種選擇: ①間接操作 先通過KVC方法取到集合屬性,然後通過集合屬性操作集合中的元素。 ②直接操作 蘋果為我們提供了一些方法模闆,我們可以以規定的格式實作這些方法來達到通路集合屬性中元素的目的。 有序集合對應方法如下:

  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. -insertObject:in<Key>AtIndex:
  9. -insert<Key>:atIndexes:
  10. //兩個必須實作一個,類似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
  11. -removeObjectFrom<Key>AtIndex:
  12. -remove<Key>AtIndexes:
  13. //兩個必須實作一個,類似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
  14. -replaceObjectIn<Key>AtIndex:withObject:
  15. -replace<Key>AtIndexes:with<Key>:

無序集合對應方法如下:

  1. -countOf<Key>
  2. //必須實作,對應于NSArray的基本方法count:
  3. -objectIn<Key>AtIndex:

 -<key>AtIndexes:

 //這兩個必須實作一個,對應于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

  -get<Key>:range:

  //不是必須實作的,但實作後可以提高性能,其對應于 NSArray 方法 getObjects:range:

  -insertObject:in<Key>AtIndex:

  -insert<Key>:atIndexes:

  //兩個必須實作一個,類似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

  -removeObjectFrom<Key>AtIndex:

  -remove<Key>AtIndexes:

  //兩個必須實作一個,類似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

  -replaceObjectIn<Key>AtIndex:withObject:

  -replace<Key>AtIndexes:with<Key>:

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

KVC提供了驗證Key對應的Value是否可用的方法:

  1. - (BOOL)validateValue:(inout id *)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

該方法預設的實作是調用一個如下格式的方法:

  1. - (BOOL)validate<Key>:error:

比如屬性name對應的方法為:

  1. -(BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError {
  2.     // Implementation specific code.
  3.     return ...;
  4. }

這樣就給了我們一次糾錯的機會。 需要指出的是,KVC是不會自動調用鍵值驗證方法的,就是說我們需要手動驗證。但是有些技術,比如CoreData會自動調用。

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

KVC可以自動的将數值或結構體型的資料打包或解包成NSNumber或NSValue對象,以達到适配的目的。

舉個例子,Person類有個NSInteger類型的age屬性 ①修改值 我們通過KVC技術使用如下方式設定age屬性的值:

  1. [person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];

我們賦給age的是一個NSNumber對象,KVC會自動的将NSNumber對象轉換成NSInteger對象,然後再調用相應的通路器方法設定age的值。

②擷取值 同樣,以如下方式擷取age屬性值:

  1. [person valueForKey:@"age"];

這時,會以NSNumber的形式傳回age的值。 需要說明的是,什麼時候傳回的是NSNumber,什麼時候傳回的是NSValue? ③使用NSNumber封裝 可以使用NSNumber的資料類型有:

  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主要用于處理結構體型的資料,它本身提供了如下集中結構的支援:

  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對象的,具體實作方法參見另一篇文章: http://blog.csdn.net/wzzvictory/article/details/8614433

6、集合運算符(Collection Operators)

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

ios-kvc\kvo 原理

①簡單集合運算符 簡單集合運算符共有@avg,@count,@max,@min,@sum5種,都表示啥不用我說了吧,目前還不支援自定義。 有一個集合類的對象:transactions,它存儲了一個個的Transaction類的執行個體,該類有三個屬性:payee,amount,date。下面以此為例說明如何使用這些運算符: 要擷取amount的平均值可以這樣:

  1. NSNumber *transactionAverage = [transactions valueForKeyPath:@"@avg.amount"];

要擷取transactions集合中元素數目可以這樣:

  1. NSNumber *numberOfTransactions = [transactions valueForKeyPath:@"@count"];

需要之處的是,@count是這些集合運算符中比較特殊的一個,因為它沒有右路經,原因很容易了解。 ②對象運算符 比集合運算符稍微複雜,能以數組的方式傳回指定的内容,一共有兩種

  1. @distinctUnionOfObjects
  2. @unionOfObjects

它們的傳回值都是NSArray,差別是前者傳回的元素都是唯一的,是去重以後的結果;後者傳回的元素是全集。 用法如下:

  1. NSArray *payees = [transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
  2. NSArray *payees = [transactions valueForKeyPath:@"@unionOfObjects.payee"];

前者會将收款人的姓名去除重複的以後傳回,後者直接傳回所有收款人的姓名。 ③Array和Set操作符 這種情況更複雜了,說的是集合中包含集合的情況,我們執行了如下的一段代碼:

  1. // Create the array that contains additional arrays.
  2. self.arrayOfTransactionsArray = [NSMutableArray array];
  3. // Add the array of objects used in the above examples.
  4. [arrayOfTransactionsArray addObject:transactions];
  5. // Add a second array of objects; this array contains alternate values.
  6. [arrayOfTransactionsArrays addObject:moreTransactions];

得到了一個包含集合的集合:arrayOfTransactionsArray 這時如果我們想操作arrayOfTransactionsArray中包含的集合中的元素時,可以使用如下三個運算符:

  1. @distinctUnionOfArrays
  2. @unionOfArrays
  3. @distinctUnionOfSets

前兩個針對的集合是Arrays,後一個針對的集合是Sets。因為Sets中的元素本身就是唯一的,是以沒有對應的@unionOfSets運算符。 它們的用法舉例如下:

  1. NSArray *payees = [arrayOfTransactionsArrays valueForKeyPath:@"@unionOfArrays.payee"];

三、實作原理

1、KVC如何通路屬性值

KVC再某種程度上提供了通路器的替代方案。不過通路器方法是一個很好的東西,以至于隻要是有可能,KVC也盡量再通路器方法的幫助下工作。為了設定或者傳回對象屬性,KVC按順序使用如下技術: ①檢查是否存在-、-is(隻針對布爾值有效)或者-get的通路器方法,如果有可能,就是用這些方法傳回值; 檢查是否存在名為-set:的方法,并使用它做設定值。對于-get和-set:方法,将大寫Key字元串的第一個字母,并與Cocoa的方法命名保持一緻; ②如果上述方法不可用,則檢查名為-_、-_is(隻針對布爾值有效)、-_get和-_set:方法; ③如果沒有找到通路器方法,可以嘗試直接通路執行個體變量。執行個體變量可以是名為:或_; ④如果仍為找到,則調用valueForUndefinedKey:和setValue:forUndefinedKey:方法。這些方法的預設實作都是抛出異常,我們可以根據需要重寫它們。

2、KVC/KVO實作原理

鍵值編碼和鍵值觀察是根據isa-swizzling技術來實作的,主要依據runtime的強大動态能力。下面的這段話是引自網上的一篇文章: http://blog.csdn.net/kesalin/article/details/8194240 當某個類的對象第一次被觀察時,系統就會在運作期動态地建立該類的一個派生類,在這個派生類中重寫基類中任何被觀察屬性的 setter 方法。

派生類在被重寫的 setter 方法實作真正的通知機制,就如前面手動實作鍵值觀察那樣。這麼做是基于設定屬性會調用 setter 方法,而通過重寫就獲得了 KVO 需要的通知機制。當然前提是要通過遵循 KVO 的屬性設定方式來變更屬性值,如果僅是直接修改屬性對應的成員變量,是無法實作 KVO 的。

同時派生類還重寫了 class 方法以“欺騙”外部調用者它就是起初的那個類。然後系統将這個對象的 isa 指針指向這個新誕生的派生類,是以這個對象就成為該派生類的對象了,因而在該對象上對 setter 的調用就會調用重寫的 setter,進而激活鍵值通知機制。此外,派生類還重寫了 dealloc 方法來釋放資源。 原文寫的很好,還舉了解釋性的例子,大家可以去看看。 在我之前的一篇介紹Objective-C類和元類的文章: http://blog.csdn.net/wzzvictory/article/details/8592492 中介紹過,isa指針指向的其實是類的元類,如果之前的類名為:Person,那麼被runtime更改以後的類名會變成:NSKVONotifying_Person。 新的NSKVONotifying_Person類會重寫以下方法: 增加了監聽的屬性對應的set方法,class,dealloc,_isKVOA。 ①class 重寫class方法是為了我們調用它的時候傳回跟重寫繼承類之前同樣的内容。 列印如下内容:

  1. NSLog(@"self->isa:%@",self->isa);
  2. NSLog(@"self class:%@",[self class]);

在建立KVO監聽前,列印結果為:

  1. self->isa:Person
  2. self class:Person

在建立KVO監聽之後,列印結果為:

  1. self->isa:NSKVONotifying_Person
  2. self class:Person
  1. self->isa:NSKVONotifying_Person
  2. self class:Person

這也是isa指針和class方法的一個差別,大家使用的時候注意。 ②重寫set方法 新類會重寫對應的set方法,是為了在set方法中增加另外兩個方法的調用:

  1. - (void)willChangeValueForKey:(NSString *)key
  2. - (void)didChangeValueForKey:(NSString *)key

其中,didChangeValueForKey:方法負責調用:

  1. - (void)observeValueForKeyPath:(NSString *)keyPath
  2.                       ofObject:(id)object
  3.                         change:(NSDictionary *)change
  4.                        context:(void *)context

方法,這就是KVO實作的原理了! 如果沒有任何的通路器方法,-setValue:forKey方法會直接調用:

  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的,詳情可以參考這篇文章: http://www.mikeash.com/pyblog/key-value-observing-done-right.html 核心思想是說KVO的回調機制,不能傳一個selector或者block作為回調,而必須重寫-addObserver:forKeyPath:options:context:方法所引發的一系列問題。為了解決這個問題,作者還親自實作了一個MAKVONotificationCenter類,代碼見github: https://github.com/mikeash/MAKVONotificationCenter 不過個人認為這隻是蘋果做的KVO不夠完美,不能算是缺陷。   參考文檔: http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/KeyValueCoding/Articles/KeyValueCoding.html#//apple_ref/doc/uid/10000107-SW1 http://blog.csdn.net/kesalin/article/details/8194240

作者: wangzz 原文位址: http://blog.csdn.net/wzzvictory/article/details/9674431

posted on 2015-11-02 16:20 城之内 閱讀( ...) 評論( ...) 編輯 收藏

轉載于:https://www.cnblogs.com/HypeCheng/p/4930493.html