
目錄
- 1. 什么是 KVC
- 2. 訪問對象屬性
- 3. 訪問集合屬性
- 4. 使用集合運算符
- 5. 自定義集合運算符
- 6. 非對象值處理
- 7. 屬性驗證
- 8. 搜索規(guī)則
- 9. 異常處理
- 10. 相關(guān)面試題
- 參考
1. 什么是 KVC
-
KVC的全稱是Key-Value Coding(鍵值編碼),是由NSKeyValueCoding非正式協(xié)議啟用的一種機制,對象采用這種機制來提供對其屬性的間接訪問,可以通過字符串來訪問一個對象的成員變量或其關(guān)聯(lián)的存取方法(getter or setter)。 - 通常,我們可以直接通過存取方法或變量名來訪問對象的屬性。我們也可以使用
KVC間接訪問對象的屬性,并且KVC還可以訪問私有變量。某些情況下,KVC還可以幫助簡化代碼。 -
KVC是許多其他 Cocoa 技術(shù)的基礎(chǔ)概念,比如 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 來賦值
基礎(chǔ)操作
如下是 BankAccount 類的聲明:
@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設(shè)置值。
[myAccount setValue:@(100.0) forKey:@"currentBalance"];
KeyPath
KVC還支持多級訪問,KeyPath用法跟點語法相同。
例如:我們想對myAccount的owner屬性的address屬性的street屬性賦值,其KeyPath為owner.address.street。
[myAccount setValue:@"地址" forKeyPath:@"owner.address.street"];
多值操作
給定一組Key,獲得一組value,以字典的形式返回。該方法為數(shù)組中的每個Key調(diào)用valueForKey:方法。
- (NSDictionary<NSString *,id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
將指定字典中的值設(shè)置到消息接收者的屬性中,使用字典的Key標識屬性。默認實現(xiàn)是為每個鍵值對調(diào)用setValue:forKey:方法 ,會根據(jù)需要用nil替換NSNull對象。
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id> *)keyedValues;
3. 訪問集合屬性
我們可以像訪問其它對象一樣使用valueForKey:或setValue:forKey:方法來獲取或設(shè)置集合對象(主要指NSArray和NSSet)。但是,當我們要操作集合對象的內(nèi)容,比如添加或者刪除元素時,通過KVC的可變代理方法獲取集合代理對象是最有效的。
根據(jù)KVO的實現(xiàn)原理,是在運行時動態(tài)生成子類并重寫setter方法來達到可以通知所有觀察者對象的目的,因此我們對集合對象進行操作是不會觸發(fā)KVO的。當我們要使用KVO監(jiān)聽集合對象變化時,需要通過KVC的可變代理方法獲取集合代理對象,然后對代理對象進行操作。當代理對象的內(nèi)部對象發(fā)生改變時,會觸發(fā)KVO的監(jiān)聽方法。
傳送門:iOS - 關(guān)于 KVO 的一些總結(jié)
KVC提供了三種不同的代理對象訪問的代理方法,每種都有Key和KeyPath兩種方法。
-
mutableArrayValueForKey:和mutableArrayValueForKeyPath:返回
NSMutableArray對象的代理對象。 -
mutableSetValueForKey:和mutableSetValueForKeyPath:返回
NSMutableSet對象的代理對象。 -
mutableOrderedSetValueForKey:和mutableOrderedSetValueForKeyPath:返回
NSMutableOrderedSet對象的代理對象。
4. 使用集合運算符
KVC的valueForKeyPath:方法除了可以取出屬性值以外,還可以在KeyPath中嵌套集合運算符,來對集合對象進行操作。
以下是KeyPath集合運算符的格式,主要分為 3 個部分,如圖 4-1。
- Left key path:左鍵路徑,要操作的集合對象,如果消息接收者就是集合對象,則可以省略 Left 部分;
- Collection operator:集合運算符;
- Right key path:右鍵路徑,要進行運算的集合中的屬性。

集合運算符主要分為三類:
- ① 聚合運算符:以某種方式合并集合中的對象,并返回右鍵路徑中指定的屬性的數(shù)據(jù)類型匹配的一個對象,一般返回
NSNumber實例。 - ② 數(shù)組運算符:根據(jù)運算符的條件,將符合條件的對象以一個
NSArray實例返回。 - ③ 嵌套運算符:處理集合對象中嵌套其他集合對象的情況,并根據(jù)運算符返回一個
NSArray或NSSet實例。
示例
如下是 BankAccount 類和 Transaction 類的聲明。BankAccount 中有一個 transactions 數(shù)組屬性,其元素為 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 數(shù)組的數(shù)據(jù)。
| 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 |
聚合運算符
以某種方式合并集合中的對象,并返回右鍵路徑中指定的屬性的數(shù)據(jù)類型匹配的一個對象,一般返回NSNumber實例。
@avg
讀取集合中每個元素的右鍵路徑指定的屬性,將其轉(zhuǎn)換為double類型 (nil用 0 替代),并計算這些值的算術(shù)平均值。然后將結(jié)果以NSNumber實例返回。
// 計算上表中 amount 的平均值。
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
// transactionAverage 格式化的結(jié)果為 $ 456.54。
@count
計算集合中的元素個數(shù),以NSNumber實例返回。
// 計算 transactions 集合中的元素個數(shù)。
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
// numberOfTransactions 的值為 13。
備注:
@count運算符比較特別,它不需要寫右鍵路徑,即使寫了也會被忽略。
@sum
讀取集合中每個元素的右鍵路徑指定的屬性,將其轉(zhuǎn)換為double類型 (nil用 0 替代),并計算這些值的總和。然后將結(jié)果以NSNumber實例返回。
// 計算上表中 amount 的總和。
NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
// amountSum 的結(jié)果為 $ 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根據(jù)右鍵路徑指定的屬性在集合中搜索,搜索使用compare:方法進行比較,許多基礎(chǔ)類 (如NSNumber類) 中都有定義。因此,右鍵路徑指定的屬性必須能響應(yīng)compare:消息。搜索忽略值為nil的集合項??梢酝ㄟ^重寫compare:方法對搜索過程進行控制。
數(shù)組運算符
根據(jù)運算符的條件,將符合條件的對象以一個NSArray實例返回。
@unionOfObjects
讀取集合中每個元素的右鍵路徑指定的屬性,放在一個NSArray實例中并返回。
// 獲取集合中的所有 payee 對象。
NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
// payees 數(shù)組包含以下字符串: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實例中,將數(shù)組進行去重后返回。
// 獲取集合中的所有不同的 payee 對象。
NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
// distinctPayees 數(shù)組包含以下字符串:Car Loan, General Cable, Animal Hospital, Green Power, Mortgage。
注意: 在使用數(shù)組運算符時,如果有任何操作的對象為
nil,則valueForKeyPath:方法將引發(fā)異常。
嵌套運算符
處理集合對象中嵌套其他集合對象的情況,并根據(jù)運算符返回一個NSArray或NSSet實例。
如下 moreTransactions 是裝著 transaction 對象的數(shù)組,arrayOfArrays 數(shù)組中嵌套了 self.transactions 和 moreTransactions 兩個數(shù)組。
NSArray* moreTransactions = @[<# transaction data #>];
NSArray* arrayOfArrays = @[self.transactions, moreTransactions];
下表是 moreTransactions 數(shù)組的數(shù)據(jù)。
| 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 數(shù)組包含以下字符串: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實例中,將數(shù)組進行去重后返回。
// 獲取 arrayOfArrays 集合中的每個集合中的所有不同的 payee 對象。
NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
// collectedDistinctPayees 數(shù)組包含以下字符串:Hobby Shop, Mortgage, Animal Hospital, Second Mortgage, Car Loan, General Cable - Cottage, General Cable, Green Power.
@distinctUnionOfSets
讀取集合中的每個集合中的每個元素的右鍵路徑指定的屬性,放在一個NSSet實例中,去重后返回。
NSSet *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfSets.payee"];
注意:
- 在使用嵌套運算符時,
valueForKeyPath:內(nèi)部會根據(jù)運算符創(chuàng)建一個NSMutableArray或NSMutableSet對象,將集合中的array和set添加進去再進行操作。如果集合中有非集合元素,會導(dǎo)致Crash。- 使用
unionOfArrays或distinctUnionOfArrays運算符,消息接收者應(yīng)該是arrayOfArrays類型,即NSArray< NSArray* >* arrayOfArrays;;使用distinctUnionOfSets運算符,消息接收者應(yīng)該是setOfSets或者arrayOfSets類型。否則會發(fā)生異常。- 在使用嵌套運算符時,如果有任何操作的對象為
nil, 則valueForKeyPath:方法將引發(fā)異常。
拓展
如果集合中的對象都是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:
......
方法很多,我們搜索關(guān)鍵字avg、count、sum等KVC為我們提供的集合運算符,發(fā)現(xiàn)都有對應(yīng)的方法_<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并導(dǎo)致程序崩潰,reason:[<__NSSetI 0x6000017a12f0> valueForKeyPath:]: this class does not implement the unionOfArrays operation.不支持該運算符。而
NSArray類雖然支持@distinctUnionOfSets運算符,但其必須是arrayOfSets類型,即NSArray< NSSet* >* arrayOfSets;。因為_distinctUnionOfSetsForKeyPath方法中會創(chuàng)建一個NSMutableSet實例,并調(diào)用unionSet:方法將集合中的set的元素添加進去再進行操作。如果是arrayOfArrays類型就會拋出異常NSInvalidArgumentException并導(dǎo)致程序崩潰,reason:'*** -[NSMutableSet unionSet:]: set argument is not an NSSet'即集合中有非NSSet元素。
我們嘗試為NSArray添加一個分類,并定義一個_medianForKeyPath:方法,用來獲取NSArray中的中位數(shù)。
#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支持基礎(chǔ)數(shù)據(jù)類型和結(jié)構(gòu)體,在使用KVC進行賦值或取值的時候,會自動在非對象值和對象值之間進行轉(zhuǎn)換。
- 當進行取值如
valueForKey:時,如果返回值非對象,會使用該值初始化一個NSNumber(用于基礎(chǔ)數(shù)據(jù)類型)或NSValue(用于結(jié)構(gòu)體)實例,然后返回該實例。 - 當進行賦值如
setValue:forKey:時,如果key的數(shù)據(jù)類型非對象,則會發(fā)送一條<type>Value消息給value對象以提取基礎(chǔ)數(shù)據(jù),然后賦值給key。
注意:
- 因為
Swift中的所有屬性都是對象,所以這里僅適用于Objective-C屬性。- 當進行賦值如
setValue:forKey:時,如果key的數(shù)據(jù)類型是非對象類型,則value就禁止傳nil。否則會調(diào)用setNilValueForKey:方法,該方法的默認實現(xiàn)拋出異常NSInvalidArgumentException,并導(dǎo)致程序Crash。
下表是KVC對于基礎(chǔ)數(shù)據(jù)類型和NSNumber對象之間的轉(zhuǎn)換。
| 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對于結(jié)構(gòu)體類型和NSValue對象之間的轉(zhuǎn)換。
| Data type | Creation method | Accessor method |
|---|---|---|
| CGPoint | valueWithCGPoint: | CGPointValue |
| CGRect | valueWithCGRect: | CGRectValue |
| CGSize | valueWithCGSize: | CGSizeValue |
| NSRange | valueWithRange: | rangeValue |
除了以上CGPoint、CGRect、CGSize和NSRange類型的結(jié)構(gòu)體可以和NSValue對象之間進行轉(zhuǎn)換,我們自定義的結(jié)構(gòu)體也可以包裝成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方法的默認實現(xiàn)是查看消息接收者類中是否實現(xiàn)了遵循命名規(guī)則為validate<Key>:error:的方法,如果有的話就返回調(diào)用該方法的結(jié)果;如果沒有的話,則默認驗證成功并返回YES。我們可以在消息接收者類中實現(xiàn)validate<Key>:error:的方法來自定義邏輯返回YES或NO。
- (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類中實現(xiàn)了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;
}
// 打?。?
備注: 默認情況下,
KVC是不會自動驗證屬性的。
8. 搜索規(guī)則
除了了解KVC的使用,了解KVC取值和賦值過程的工作原理也是很有必要的。
基本的 Getter 搜索模式
以下是valueForKey:方法的默認實現(xiàn),給定一個key作為輸入?yún)?shù),在消息接收者類中操作,執(zhí)行以下過程。
- ① 按照
get<Key>、<key>、is<Key>、_<key>順序查找方法。
如果找到就調(diào)用取值并執(zhí)行⑤,否則執(zhí)行②; - ② 查找
countOf<Key>、objectIn<Key>AtIndex:、<key>AtIndexes:命名的方法。
如果找到第一個和后面兩個中的至少一個,則創(chuàng)建一個能夠響應(yīng)所有NSArray的方法的集合代理對象(類型為NSKeyValueArray,繼承自NSArray),并返回該對象。否則執(zhí)行③;- 代理對象隨后將其接收到的任何
NSArray消息轉(zhuǎn)換為countOf<Key>、objectIn<Key>AtIndex:、<Key>AtIndexes:消息的組合,并將其發(fā)送給KVC調(diào)用方。如果原始對象還實現(xiàn)了一個名為get<Key>:range:的可選方法,則代理對象也會在適當時使用該方法。 - 當
KVC調(diào)用方與代理對象一起工作時,允許底層屬性的行為如同NSArray一樣,即使它不是NSArray。
- 代理對象隨后將其接收到的任何
- ③ 查找
countOf<Key>、enumeratorOf<Key>、memberOf<Key>:命名的方法。
如果三個方法都找到,則創(chuàng)建一個能夠響應(yīng)所有NSSet的方法的集合代理對象(類型為NSKeyValueSet,繼承自NSSet),并返回該對象。否則執(zhí)行④;- 代理對象隨后將其接收到的任何
NSSet消息轉(zhuǎn)換為countOf<Key>、enumeratorOf<Key>、memberOf<Key>:消息的組合,并將其發(fā)送給KVC調(diào)用方。 - 當
KVC調(diào)用方與代理對象一起工作時,允許底層屬性的行為如同NSSet一樣,即使它不是NSSet。
- 代理對象隨后將其接收到的任何
- ④ 查看消息接收者類的
+accessInstanceVariablesDirectly方法的返回值(默認返回YES)。如果返回YES,就按照_<key>、_is<Key>、<key>、is<Key>順序查找成員變量。如果找到就直接取值并執(zhí)行⑤,否則執(zhí)行⑥。如果+accessInstanceVariablesDirectly方法返回NO也執(zhí)行⑥。 - ⑤ 如果取到的值是一個對象指針,即獲取的是對象,則直接將對象返回。
? 如果取到的值是一個NSNumber支持的數(shù)據(jù)類型,則將其存儲在NSNumber實例并返回。
? 如果取到的值不是一個NSNumber支持的數(shù)據(jù)類型,則轉(zhuǎn)換為NSValue對象, 然后返回。 - ⑥ 調(diào)用
valueForUndefinedKey:方法,該方法拋出異常NSUnknownKeyException,并導(dǎo)致程序Crash。這是默認實現(xiàn),我們可以重寫該方法根據(jù)特定key做一些特殊處理。
基本的 Setter 搜索模式
以下是setValue:forKey:方法的默認實現(xiàn),給定key和value作為輸入?yún)?shù),嘗試將KVC調(diào)用方的屬性名為key的值設(shè)置為value,執(zhí)行以下過程。
- ① 按照
set<Key>:、_set<Key>:順序查找方法。
如果找到就調(diào)用并將value傳進去(根據(jù)需要進行數(shù)據(jù)類型轉(zhuǎn)換),否則執(zhí)行②。 - ② 查看消息接收者類的
+accessInstanceVariablesDirectly方法的返回值(默認返回YES)。如果返回YES,就按照_<key>、_is<Key>、<key>、is<Key>順序查找成員變量(同 基本的Getter搜索模式)。如果找到就將value賦值給它(根據(jù)需要進行數(shù)據(jù)類型轉(zhuǎn)換),否則執(zhí)行③。如果+accessInstanceVariablesDirectly方法返回NO也執(zhí)行③。 - ③ 調(diào)用
setValue:forUndefinedKey:方法,該方法拋出異常NSUnknownKeyException,并導(dǎo)致程序Crash。這是默認實現(xiàn),我們可以重寫該方法根據(jù)特定key做一些特殊處理。
NSMutableArray 搜索模式
以下是mutableArrayValueForKey:方法的默認實現(xiàn),給定一個key作為輸入?yún)?shù),返回屬性名為key的集合的代理對象(這里指NSMutableArray對象),在消息接收者類中操作,執(zhí)行以下過程。
-
① 查找一對方法
insertObject:in<Key>AtIndex:和removeObjectFrom<Key>AtIndex:
(相當于NSMutableArray的原始方法insertObject:atIndex:和removeObjectAtIndex:),
或者insert<Key>:atIndexes:和remove<Key>AtIndexes:
(相當于NSMutableArray的原始方法insertObjects:atIndexes:和removeObjectsAtIndexes:)。- 如果我們至少實現(xiàn)了一個
insertion方法和一個removal方法,則返回一個代理對象,來響應(yīng)發(fā)送給NSMutableArray的消息,通過發(fā)送insertObject:in<Key>AtIndex:、removeObjectFrom<Key>AtIndex:、insert<Key>:atIndexes:、remove<Key>AtIndexes:組合消息給KVC調(diào)用方。否則執(zhí)行②。
該代理對象類型為
NSKeyValueFastMutableArray2,繼承鏈為NSKeyValueFastMutableArray2->NSKeyValueFastMutableArray->NSKeyValueMutableArray->NSMutableArray。- 如果我們也實現(xiàn)了一個可選的
replace object方法,如replaceObjectIn<Key>AtIndex:withObject:或replace<Key>AtIndexes:with<Key>:,代理對象在適當?shù)那闆r下也會使用它們,以獲得最佳性能。
- 如果我們至少實現(xiàn)了一個
-
② 查找
set<Key>:方法。
如果找到,就會向KVC調(diào)用方發(fā)送一個set<Key>:消息,來返回一個響應(yīng)NSMutableArray消息的代理對象。否則執(zhí)行③。該代理對象類型為
NSKeyValueSlowMutableArray,繼承鏈為NSKeyValueSlowMutableArray->NSKeyValueMutableArray->NSMutableArray。
注意:
此步驟中描述的機制比上一步的效率低得多,因為它可能重復(fù)創(chuàng)建新的集合對象,而不是修改現(xiàn)有的集合對象。因此,在設(shè)計自己的鍵值編碼兼容對象時,通常應(yīng)該避免使用它。
給代理對象發(fā)送NSMutableArray消息都會調(diào)用set<Key>:方法。即,對代理對象進行修改,都是調(diào)用set<Key>:來重新賦值,所以效率會低很多。
- ③ 查看消息接收者類的
+accessInstanceVariablesDirectly方法的返回值(默認返回YES)。如果返回YES,就按照_<key>、<key>順序查找成員變量。如果找到就返回一個代理對象,該代理對象將接收所有NSMutableArray消息,通常是NSMutableArray或其子類。否則執(zhí)行④。如果+accessInstanceVariablesDirectly方法返回NO也執(zhí)行④。 - ④ 返回一個可變的集合代理對象。當它接收到
NSMutableArray消息時,發(fā)送一個valueForUndefinedKey:消息給KVC調(diào)用方,該方法拋出異常NSUnknownKeyException,并導(dǎo)致程序Crash。這是默認實現(xiàn),我們可以重寫該方法根據(jù)特定key做一些特殊處理。
其他搜索模式
除了以上三種,KVC還有NSMutableSet和NSMutableOrderedSet兩種搜索模式,它們的搜索規(guī)則和NSMutableArray相同,只是搜索和調(diào)用的方法不同。具體可以查看KVC官方文檔 KVC - Accessor Search Patterns。
9. 異常處理
- ① 根據(jù)
KVC搜索規(guī)則,當沒有搜索到對應(yīng)的key或者keyPath相關(guān)方法或者變量時,會調(diào)用對應(yīng)的異常方法valueForUndefinedKey:或setValue:forUndefinedKey:,這兩個方法的默認實現(xiàn)是拋出異常NSUnknownKeyException,并導(dǎo)致程序Crash。我們可以重寫這兩個方法來處理異常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- ② 當進行賦值如
setValue:forKey:時,如果key的數(shù)據(jù)類型是非對象類型,則value就禁止傳nil。否則會調(diào)用setNilValueForKey:方法,該方法的默認實現(xiàn)是拋出異常NSInvalidArgumentException,并導(dǎo)致程序Crash。我們可以重寫這個方法來處理異常。
- (void)setNilValueForKey:(NSString *)key
{
if ([key isEqualToString:@"hidden"]) {
[self setValue:@(NO) forKey:@”hidden”];
} else {
[super setNilValueForKey:key];
}
}
10. 相關(guān)面試題
Q:通過 KVC 修改屬性會觸發(fā) KVO 嗎?
會,通過KVC修改成員變量值也會觸發(fā)KVO。
Q:通過 KVC 鍵值編碼技術(shù)是否會破壞面向?qū)ο蟮木幊谭椒?,或者說違背面向?qū)ο蟮木幊趟枷肽兀?/h4>
valueForKey:和setValue:forKey:這里面的key是沒有任何限制的,當我們知道一個類或?qū)嵗鼉?nèi)部的私有變量名稱的情況下,我們在外界可以通過已知的key來對它的私有變量進行訪問或者賦值的操作,從這個角度來講KVC鍵值編碼技術(shù)會違背面向?qū)ο蟮木幊趟枷搿?/p>