iOS - 關於 KVC 的一些總結

1. 什麼是 KVC

  • KVC的全稱是Key-Value Coding(鍵值編碼),是由NSKeyValueCoding非正式協議啓用的一種機制,對象採用這種機制來提供對其屬性的間接訪問,能夠經過字符串來訪問一個對象的成員變量或其關聯的存取方法(getter or setter)。
  • 一般,咱們能夠直接經過存取方法或變量名來訪問對象的屬性。咱們也可使用KVC間接訪問對象的屬性,而且KVC還能夠訪問私有變量。某些狀況下,KVC還能夠幫助簡化代碼。
  • KVC是許多其餘 Cocoa 技術的基礎概念,好比 KVO、Cocoa bindings、Core Data、AppleScript-ability 等等。

2. 訪問對象屬性

經常使用 API

- (nullable id)valueForKey:(NSString *)key;         // 經過 key 來取值
- (nullable id)valueForKeyPath:(NSString *)keyPath; // 經過 keyPath 來取值

- (void)setValue:(nullable id)value forKey:(NSString *)key;         // 經過 key 來賦值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // 經過 keyPath 來賦值
複製代碼

基礎操做

以下是 BankAccount 類的聲明:html

@interface BankAccount : NSObject
@property (nonatomic) NSNumber* currentBalance;              // An attribute
@property (nonatomic) Person* owner;                         // A to-one relation
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many relation
@end
複製代碼

對於 BankAccount 的實例對象myAccount。 咱們可使用setter方法爲currentBalance屬性賦值,這是直接的,但缺少靈活性。面試

[myAccount setCurrentBalance:@(100.0)];
複製代碼

咱們也能夠經過KVC間接爲currentBalance屬性賦值,經過其鍵Key設置值。編程

[myAccount setValue:@(100.0) forKey:@"currentBalance"];
複製代碼

KeyPath

KVC還支持多級訪問,KeyPath用法跟點語法相同。 例如:咱們想對myAccountowner屬性的address屬性的street屬性賦值,其KeyPathowner.address.street數組

[myAccount setValue:@"地址" forKeyPath:@"owner.address.street"];
複製代碼

多值操做

給定一組Key,得到一組value,以字典的形式返回。該方法爲數組中的每一個Key調用valueForKey:方法。markdown

- (NSDictionary<NSString *,id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
複製代碼

將指定字典中的值設置到消息接收者的屬性中,使用字典的Key標識屬性。默認實現是爲每一個鍵值對調用setValue:forKey:方法 ,會根據須要用nil替換NSNull對象。app

- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id> *)keyedValues;
複製代碼

3. 訪問集合屬性

咱們能夠像訪問其它對象同樣使用valueForKey:setValue:forKey:方法來獲取或設置集合對象(主要指NSArrayNSSet)。可是,當咱們要操做集合對象的內容,好比添加或者刪除元素時,經過KVC的可變代理方法獲取集合代理對象是最有效的。
根據KVO的實現原理,是在運行時動態生成子類並重寫setter方法來達到能夠通知全部觀察者對象的目的,所以咱們對集合對象進行操做是不會觸發KVO的。當咱們要使用KVO監聽集合對象變化時,須要經過KVC的可變代理方法獲取集合代理對象,而後對代理對象進行操做。當代理對象的內部對象發生改變時,會觸發KVO的監聽方法。
傳送門:iOS - 關於 KVO 的一些總結ide

KVC提供了三種不一樣的代理對象訪問的代理方法,每種都有KeyKeyPath兩種方法。oop

  • mutableArrayValueForKey: 和 mutableArrayValueForKeyPath:post

    返回NSMutableArray對象的代理對象。性能

  • mutableSetValueForKey: 和 mutableSetValueForKeyPath:

    返回NSMutableSet對象的代理對象。

  • mutableOrderedSetValueForKey: 和 mutableOrderedSetValueForKeyPath:

    返回NSMutableOrderedSet對象的代理對象。

4. 使用集合運算符

KVCvalueForKeyPath:方法除了能夠取出屬性值之外,還能夠在KeyPath中嵌套集合運算符,來對集合對象進行操做。

如下是KeyPath集合運算符的格式,主要分爲 3 個部分。

  • Left key path:左鍵路徑,要操做的集合對象,若是消息接收者就是集合對象,則能夠省略 Left 部分;
  • Collection operator:集合運算符;
  • Right key path:右鍵路徑,要進行運算的集合中的屬性。

圖 4-1 KeyPath 集合運算符格式.png

集合運算符主要分爲三類:

  • ① 聚合運算符:以某種方式合併集合中的對象,並返回右鍵路徑中指定的屬性的數據類型匹配的一個對象,通常返回NSNumber實例。
  • ② 數組運算符:根據運算符的條件,將符合條件的對象以一個NSArray實例返回。
  • ③ 嵌套運算符:處理集合對象中嵌套其餘集合對象的狀況,並根據運算符返回一個NSArrayNSSet實例。

示例

以下是 BankAccount 類和 Transaction 類的聲明。BankAccount 中有一個 transactions 數組屬性,其元素爲 Transaction 類型。Transaction 類中定義了 3 個屬性,分別爲收款人、金額、日期。

@interface BankAccount : NSObject
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many 
@end

@interface Transaction : NSObject
@property (nonatomic) NSString* payee;   // To whom
@property (nonatomic) NSNumber* amount;  // How much
@property (nonatomic) NSDate* date;      // When
@end
複製代碼

下表是爲了演示集合運算符使用而給出的 transactions 數組的數據。

payee amount date
Green Power $120.00 Dec 1, 2015
Green Power $150.00 Jan 1, 2016
Green Power $170.00 Feb 1, 2016
Car Loan $250.00 Jan 15, 2016
Car Loan $250.00 Feb 15, 2016
Car Loan $250.00 Mar 15, 2016
General Cable $120.00 Dec 1, 2015
General Cable $155.00 Jan 1, 2016
General Cable $120.00 Feb 1, 2016
Mortgage $1,250.00 Jan 15, 2016
Mortgage $1,250.00 Feb 15, 2016
Mortgage $1,250.00 Mar 15, 2016
Animal Hospital $600.00 Jul 15, 2016

聚合運算符

以某種方式合併集合中的對象,並返回右鍵路徑中指定的屬性的數據類型匹配的一個對象,通常返回NSNumber實例。

@avg

讀取集合中每一個元素的右鍵路徑指定的屬性,將其轉換爲double類型 (nil用 0 替代),並計算這些值的算術平均值。而後將結果以NSNumber實例返回。

// 計算上表中 amount 的平均值。
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
// transactionAverage 格式化的結果爲 $ 456.54。
複製代碼

@count

計算集合中的元素個數,以NSNumber實例返回。

// 計算 transactions 集合中的元素個數。
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
// numberOfTransactions 的值爲 13。
複製代碼

備註:@count運算符比較特別,它不須要寫右鍵路徑,即便寫了也會被忽略。

@sum

讀取集合中每一個元素的右鍵路徑指定的屬性,將其轉換爲double類型 (nil用 0 替代),並計算這些值的總和。而後將結果以NSNumber實例返回。

// 計算上表中 amount 的總和。
NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
// amountSum 的結果爲 $ 5935.00。
複製代碼

@max

返回集合中右鍵路徑指定的屬性的最大值。

// 獲取日期的最大值。
NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
// latestDate 的值爲 Jul 15, 2016.
複製代碼

@min

返回集合中右鍵路徑指定的屬性的最小值。

// 獲取日期的最小值。
NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];
// earliestDate 的值爲 Dec 1, 2015.
複製代碼

備註:@max@min根據右鍵路徑指定的屬性在集合中搜索,搜索使用compare:方法進行比較,許多基礎類 (如NSNumber類) 中都有定義。所以,右鍵路徑指定的屬性必須能響應compare:消息。搜索忽略值爲nil的集合項。能夠經過重寫compare:方法對搜索過程進行控制。

數組運算符

根據運算符的條件,將符合條件的對象以一個NSArray實例返回。

@unionOfObjects

讀取數組中每一個元素的右鍵路徑指定的屬性,放在一個NSArray實例中並返回。

// 獲取集合中的全部 payee 對象。
NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
// payees 數組包含如下字符串:Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital。
複製代碼

@distinctUnionOfObjects

讀取數組中每一個元素的右鍵路徑指定的屬性,放在一個NSArray實例中,將數組進行去重後返回。

// 獲取集合中的全部不一樣的 payee 對象。
NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
// distinctPayees 數組包含如下字符串:Car Loan, General Cable, Animal Hospital, Green Power, Mortgage。
複製代碼

注意: 在使用數組運算符時,若是有任何操做的對象爲nil,則valueForKeyPath:方法將引起異常。

嵌套運算符

處理集合對象中嵌套其餘集合對象的狀況,並根據運算符返回一個NSArrayNSSet實例。

以下 moreTransactions 是裝着 transaction 對象的數組,arrayOfArrays 數組中嵌套了 self.transactions 和 moreTransactions 兩個數組。

NSArray* moreTransactions = @[<# transaction data #>];
NSArray* arrayOfArrays = @[self.transactions, moreTransactions];
複製代碼

下表是 moreTransactions 數組的數據。

payee amount date
General Cable - Cottage $120.00 Dec 18, 2015
General Cable - Cottage $155.00 Jan 9, 2016
General Cable - Cottage $120.00 Dec 1, 2016
Second Mortgage $1,250.00 Nov 15, 2016
Second Mortgage $1,250.00 Sep 20, 2016
Second Mortgage $1,250.00 Feb 12, 2016
Hobby Shop $600.00 Jun 14, 2016

@unionOfArrays

讀取集合中的每一個集合中的每一個元素的右鍵路徑指定的屬性,放在一個NSArray實例中並返回。

// 獲取 arrayOfArrays 集合中的每一個集合中的全部 payee 對象。
NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
// collectedPayees 數組包含如下字符串:Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital, General Cable - Cottage, General Cable - Cottage, General Cable - Cottage, Second Mortgage, Second Mortgage, Second Mortgage, Hobby Shop.
複製代碼

@distinctUnionOfArrays

讀取集合中的每一個集合中的每一個元素的右鍵路徑指定的屬性,放在一個NSArray實例中,將數組進行去重後返回。

// 獲取 arrayOfArrays 集合中的每一個集合中的全部不一樣的 payee 對象。
NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
// collectedDistinctPayees 數組包含如下字符串:Hobby Shop, Mortgage, Animal Hospital, Second Mortgage, Car Loan, General Cable - Cottage, General Cable, Green Power.
複製代碼

@distinctUnionOfSets

讀取集合中的每一個集合中的每一個元素的右鍵路徑指定的屬性,放在一個NSSet實例中,去重後返回。

NSSet *collectedDistinctPayees = [setOfSets valueForKeyPath:@"@distinctUnionOfSets.payee"];
複製代碼

注意:

  • 在使用嵌套運算符時,valueForKeyPath:內部會根據運算符建立一個NSMutableArrayNSMutableSet對象,將集合中的arrayset添加進去再進行操做。若是集合中有非集合元素,會致使Crash
  • 使用unionOfArraysdistinctUnionOfArrays運算符,消息接收者應該是arrayOfArrays類型,即NSArray< NSArray* >* arrayOfArrays;;使用distinctUnionOfSets運算符,消息接收者應該是setOfSets或者arrayOfSets類型。不然會發生異常。
  • 在使用嵌套運算符時,若是有任何操做的對象爲nil, 則valueForKeyPath:方法將引起異常。

拓展

若是集合中的對象都是NSNumber,右鍵路徑能夠用self

NSArray *array = @[@1, @2, @3, @4, @5];
    NSNumber *sum = [array valueForKeyPath:@"@sum.self"];
    NSLog(@"%d",[sum intValue]); 
複製代碼

5. 自定義集合運算符

上面介紹了KVC爲咱們提供的集合運算符,咱們能不能自定義呢?

咱們使用Runtime打印NSArray類的方法列表:

- (void)printNSArrayMethods
{
    u_int count;
    Method *methods = class_copyMethodList([NSArray class], &count);
    for (int i = 0; i < count ; i++)
    {
        Method method = methods[i];
        SEL sel = method_getName(method);
        NSLog(@"%d---%@", i, NSStringFromSelector(sel));
    }
    free(methods);
}
複製代碼
0---mr_isEqualToOutputDevicesArray:
1---mr_containsAnyOf:
2---mr_map:
3---sg_enumerateChunksOfSize:usingBlock:
4---_pas_mappedArrayWithTransform:
5---_pas_shuffledArrayUsingRng:
......
複製代碼

方法不少,咱們搜索關鍵字avgcountsumKVC爲咱們提供的集合運算符,發現都有對應的方法_<operatorKey>ForKeyPath:

267---_avgForKeyPath:
268---_countForKeyPath:
264---_sumForKeyPath:
269---_maxForKeyPath:
270---_minForKeyPath:
266---_unionOfObjectsForKeyPath:
273---_distinctUnionOfObjectsForKeyPath:
265---_unionOfArraysForKeyPath:
272---_distinctUnionOfArraysForKeyPath:
274---_distinctUnionOfSetsForKeyPath:
複製代碼

注意: 咱們再來看一下NSSet類支持哪些集合運算符:

50---_sumForKeyPath:
51---_avgForKeyPath:
52---_countForKeyPath:
53---_maxForKeyPath:
54---_minForKeyPath:
55---_distinctUnionOfArraysForKeyPath:
56---_distinctUnionOfObjectsForKeyPath:
57---_distinctUnionOfSetsForKeyPath:
複製代碼

可見NSSet類不支持@unionOfObjects@unionOfArrays運算符,若是使用了就會拋出異常NSInvalidArgumentException並致使程序崩潰,reason: [<__NSSetI 0x6000017a12f0> valueForKeyPath:]: this class does not implement the unionOfArrays operation.不支持該運算符。

NSArray類雖然支持@distinctUnionOfSets運算符,但其必須是arrayOfSets類型,即NSArray< NSSet* >* arrayOfSets;。由於_distinctUnionOfSetsForKeyPath方法中會建立一個NSMutableSet實例,並調用unionSet:方法將集合中的set的元素添加進去再進行操做。若是是arrayOfArrays類型就會拋出異常NSInvalidArgumentException並致使程序崩潰,reason: '*** -[NSMutableSet unionSet:]: set argument is not an NSSet'即集合中有非NSSet元素。

咱們嘗試爲NSArray添加一個分類,並定義一個_medianForKeyPath:方法,用來獲取NSArray中的中位數。

#import <Foundation/Foundation.h>
@interface NSArray (HTOperator)
- (NSNumber *)_medianForKeyPath:(NSString *)keyPath;
@end

#import "NSArray+HTOperator.h"
@implementation NSArray (HTOperator)
- (NSNumber *)_medianForKeyPath:(NSString *)keyPath {
    //排序
    NSArray *sortedArray = [self sortedArrayUsingSelector:@selector(compare:)];
    double median;
    if (self.count % 2 == 0) {
        NSInteger index1 = sortedArray.count * 0.5;
        NSInteger index2 = sortedArray.count * 0.5 - 1;
        median = ([[sortedArray objectAtIndex:index1] doubleValue] + [[sortedArray objectAtIndex:index2] doubleValue]) * 0.5;        
    } else {
        NSInteger index = (sortedArray.count-1) * 0.5;
        median = [[sortedArray objectAtIndex:index] doubleValue];
    }
    return [NSNumber numberWithDouble:median];
}
複製代碼

測試。

NSArray *array = @[@9, @7, @8, @2, @6, @3];
    NSNumber *num = [array valueForKeyPath:@"@median.self"];
    NSLog(@"%f",[num doubleValue]);
    // 6.500000
複製代碼

6. 非對象值處理

KVC支持基礎數據類型和結構體,在使用KVC進行賦值或取值的時候,會自動在非對象值和對象值之間進行轉換。

  • 當進行取值如valueForKey:時,若是返回值非對象,會使用該值初始化一個NSNumber(用於基礎數據類型)或NSValue(用於結構體)實例,而後返回該實例。
  • 當進行賦值如setValue:forKey:時,若是key的數據類型非對象,則會發送一條<type>Value消息給value對象以提取基礎數據,而後賦值給key

注意:

  • 由於Swift中的全部屬性都是對象,因此這裏僅適用於Objective-C屬性。
  • 當進行賦值如setValue:forKey:時,若是key的數據類型是非對象類型,則value就禁止傳nil。不然會調用setNilValueForKey:方法,該方法的默認實現拋出異常NSInvalidArgumentException,並致使程序Crash

下表是KVC對於基礎數據類型和NSNumber對象之間的轉換。

Data type Creation method Accessor method
BOOL numberWithBool: boolValue (in iOS)
charValue (in macOS)*
char numberWithChar: charValue
double numberWithDouble: doubleValue
float numberWithFloat: floatValue
int numberWithInt: intValue
long numberWithLong: longValue
long long numberWithLongLong: longLongValue
short numberWithShort: shortValue
unsigned char numberWithUnsignedChar: unsignedChar
unsigned int numberWithUnsignedInt: unsignedInt
unsigned long numberWithUnsignedLong: unsignedLong
unsigned long long numberWithUnsignedLongLong: unsignedLongLong
unsigned short numberWithUnsignedShort: unsignedShort

下表是KVC對於結構體類型和NSValue對象之間的轉換。

Data type Creation method Accessor method
CGPoint valueWithCGPoint: CGPointValue
CGRect valueWithCGRect: CGRectValue
CGSize valueWithCGSize: CGSizeValue
NSRange valueWithRange: rangeValue

除了以上CGPointCGRectCGSizeNSRange類型的結構體能夠和NSValue對象之間進行轉換,咱們自定義的結構體也能夠包裝成NSValue對象,示例以下。

typedef struct {
    float x, y, z;
} ThreeFloats;
 
@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end
複製代碼
// 取值
NSValue* result = [myClass valueForKey:@"threeFloats"];
複製代碼
// 賦值
ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];
複製代碼

7. 屬性驗證

KVC提供了屬性驗證的方法,以下。咱們能夠在使用KVC賦值前驗證可否爲這個key賦值指定value
validateValue方法的默認實現是查看消息接收者類中是否實現了遵循命名規則爲validate<Key>:error:的方法,若是有的話就返回調用該方法的結果;若是沒有的話,則默認驗證成功並返回YES。咱們能夠在消息接收者類中實現validate<Key>:error:的方法來自定義邏輯返回YESNO

- (BOOL)validateValue:(id  _Nullable *)value 
               forKey:(NSString *)key 
                error:(NSError * _Nullable *)error;

- (BOOL)validateValue:(inout id  _Nullable *)ioValue 
           forKeyPath:(NSString *)inKeyPath 
                error:(out NSError * _Nullable *)outError;
複製代碼

示例

Person類中實現了validateName:error:方法,驗證給name賦的值是否是jack

// ViewController.m
    Person *person = [[Person alloc] init];
    NSString *value = @"rose";
    NSString *key = @"name";
    NSError  *error;
    BOOL result = [person validateValue:&value forKey:key error:&error];
    
    if (error) {
        NSLog(@"error = %@", error);
        return;
    }
    NSLog(@"%d",result);

// Person.m
- (BOOL)validateName:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError
{
    NSString *name = *value;
    BOOL result = NO;
    if ([name isEqualToString:@"jack"]) {
        result = YES;
    }
    return result;
}
// 打印:0
複製代碼

備註: 默認狀況下,KVC是不會自動驗證屬性的。

8. 搜索規則

除了瞭解KVC的使用,瞭解KVC取值和賦值過程的工做原理也是頗有必要的。

基本的 Getter 搜索模式

如下是valueForKey:方法的默認實現,給定一個key做爲輸入參數,在消息接收者類中操做,執行如下過程。

  • ① 按照get<Key><key>is<Key>_<key>順序查找方法。
    若是找到就調用取值並執行⑤,不然執行②;
  • ② 查找countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:命名的方法。
    若是找到第一個和後面兩個中的至少一個,則建立一個可以響應全部NSArray的方法的集合代理對象(類型爲NSKeyValueArray,繼承自NSArray),並返回該對象。不然執行③;
    • 代理對象隨後將其接收到的任何NSArray消息轉換爲countOf<Key>objectIn<Key>AtIndex:<Key>AtIndexes:消息的組合,並將其發送給KVC調用方。若是原始對象還實現了一個名爲get<Key>:range:的可選方法,則代理對象也會在適當時使用該方法。
    • KVC調用方與代理對象一塊兒工做時,容許底層屬性的行爲如同NSArray同樣,即便它不是NSArray
  • ③ 查找countOf<Key>enumeratorOf<Key>memberOf<Key>:命名的方法。
    若是三個方法都找到,則建立一個可以響應全部NSSet的方法的集合代理對象(類型爲NSKeyValueSet,繼承自NSSet),並返回該對象。不然執行④;
    • 代理對象隨後將其接收到的任何NSSet消息轉換爲countOf<Key>enumeratorOf<Key>memberOf<Key>:消息的組合,並將其發送給KVC調用方。
    • KVC調用方與代理對象一塊兒工做時,容許底層屬性的行爲如同NSSet同樣,即便它不是NSSet
  • ④ 查看消息接收者類的+accessInstanceVariablesDirectly方法的返回值(默認返回YES)。若是返回YES,就按照_<key>_is<Key><key>is<Key>順序查找成員變量。若是找到就直接取值並執行⑤,不然執行⑥。若是+accessInstanceVariablesDirectly方法返回NO也執行⑥。
  • ⑤ 若是取到的值是一個對象指針,即獲取的是對象,則直接將對象返回。
    • 若是取到的值是一個NSNumber支持的數據類型,則將其存儲在NSNumber實例並返回。
    • 若是取到的值不是一個NSNumber支持的數據類型,則轉換爲NSValue對象, 而後返回。
  • ⑥ 調用valueForUndefinedKey:方法,該方法拋出異常NSUnknownKeyException,並致使程序Crash。這是默認實現,咱們能夠重寫該方法根據特定key作一些特殊處理。

基本的 Setter 搜索模式

如下是setValue:forKey:方法的默認實現,給定keyvalue做爲輸入參數,嘗試將KVC調用方的屬性名爲key的值設置爲value,執行如下過程。

  • ① 按照set<Key>:_set<Key>:順序查找方法。
    若是找到就調用並將value傳進去(根據須要進行數據類型轉換),不然執行②。
  • ② 查看消息接收者類的+accessInstanceVariablesDirectly方法的返回值(默認返回YES)。若是返回YES,就按照_<key>_is<Key><key>is<Key>順序查找成員變量(同 基本的 Getter 搜索模式)。若是找到就將value賦值給它(根據須要進行數據類型轉換),不然執行③。若是+accessInstanceVariablesDirectly方法返回NO也執行③。
  • ③ 調用setValue:forUndefinedKey:方法,該方法拋出異常NSUnknownKeyException,並致使程序Crash。這是默認實現,咱們能夠重寫該方法根據特定key作一些特殊處理。

NSMutableArray 搜索模式

如下是mutableArrayValueForKey:方法的默認實現,給定一個key做爲輸入參數,返回屬性名爲key的集合的代理對象(這裏指NSMutableArray對象),在消息接收者類中操做,執行如下過程。

  • ① 查找一對方法insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex:
    (至關於NSMutableArray的原始方法insertObject:atIndex:removeObjectAtIndex:),
    或者insert<Key>:atIndexes:remove<Key>AtIndexes:
    (至關於NSMutableArray的原始方法insertObjects:atIndexes:removeObjectsAtIndexes:)。

    • 若是咱們至少實現了一個insertion方法和一個removal方法,則返回一個代理對象,來響應發送給NSMutableArray的消息,經過發送insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex:insert<Key>:atIndexes:remove<Key>AtIndexes:組合消息給KVC調用方。不然執行②。

    該代理對象類型爲NSKeyValueFastMutableArray2,繼承鏈爲NSKeyValueFastMutableArray2->NSKeyValueFastMutableArray->NSKeyValueMutableArray->NSMutableArray

    • 若是咱們也實現了一個可選的replace object方法,如replaceObjectIn<Key>AtIndex:withObject:replace<Key>AtIndexes:with<Key>:,代理對象在適當的狀況下也會使用它們,以得到最佳性能。
  • ② 查找set<Key>:方法。
    若是找到,就會向KVC調用方發送一個set<Key>:消息,來返回一個響應NSMutableArray消息的代理對象。不然執行③。

    該代理對象類型爲NSKeyValueSlowMutableArray,繼承鏈爲NSKeyValueSlowMutableArray->NSKeyValueMutableArray->NSMutableArray

    注意: 此步驟中描述的機制比上一步的效率低得多,由於它可能重複建立新的集合對象,而不是修改現有的集合對象。所以,在設計本身的鍵值編碼兼容對象時,一般應該避免使用它。

    給代理對象發送NSMutableArray消息都會調用set<Key>:方法。即,對代理對象進行修改,都是調用set<Key>:來從新賦值,因此效率會低不少。

  • ③ 查看消息接收者類的+accessInstanceVariablesDirectly方法的返回值(默認返回YES)。若是返回YES,就按照_<key><key>順序查找成員變量。若是找到就返回一個代理對象,該代理對象將接收全部NSMutableArray消息,一般是NSMutableArray或其子類。不然執行④。若是+accessInstanceVariablesDirectly方法返回NO也執行④。

  • ④ 返回一個可變的集合代理對象。當它接收到NSMutableArray消息時,發送一個valueForUndefinedKey:消息給KVC調用方,該方法拋出異常NSUnknownKeyException,並致使程序Crash。這是默認實現,咱們能夠重寫該方法根據特定key作一些特殊處理。

其餘搜索模式

除了以上三種,KVC還有NSMutableSetNSMutableOrderedSet兩種搜索模式,它們的搜索規則和NSMutableArray相同,只是搜索和調用的方法不一樣。具體能夠查看KVC官方文檔 KVC - Accessor Search Patterns

9. 異常處理

  • ① 根據KVC搜索規則,當沒有搜索到對應的key或者keyPath相關方法或者變量時,會調用對應的異常方法valueForUndefinedKey:setValue:forUndefinedKey:,這兩個方法的默認實現是拋出異常NSUnknownKeyException,並致使程序Crash。咱們能夠重寫這兩個方法來處理異常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
複製代碼
  • ② 當進行賦值如setValue:forKey:時,若是key的數據類型是非對象類型,則value就禁止傳nil。不然會調用setNilValueForKey:方法,該方法的默認實現是拋出異常NSInvalidArgumentException,並致使程序Crash。咱們能夠重寫這個方法來處理異常。
- (void)setNilValueForKey:(NSString *)key
{
    if ([key isEqualToString:@"hidden"]) {
        [self setValue:@(NO) forKey:@」hidden」];
    } else {
        [super setNilValueForKey:key];
    }
}
複製代碼

10. 相關面試題

Q:經過 KVC 修改屬性會觸發 KVO 嗎?

會,經過KVC修改爲員變量值也會觸發KVO

Q:經過 KVC 鍵值編碼技術是否會破壞面向對象的編程方法,或者說違背面向對象的編程思想呢?

valueForKey:setValue:forKey:這裏面的key是沒有任何限制的,當咱們知道一個類或實例它內部的私有變量名稱的狀況下,咱們在外界能夠經過已知的key來對它的私有變量進行訪問或者賦值的操做,從這個角度來說KVC鍵值編碼技術會違背面向對象的編程思想。

參考

Key-Value Coding Programming Guide(蘋果官方文檔)

相關文章
相關標籤/搜索