一、簡介
KVC 的全稱是 Key-Value Coding(鍵值編碼)蝙泼,是由 NSKeyValueCoding
非正式協(xié)議啟用的一種機(jī)制,對(duì)象采用這種機(jī)制來提供對(duì)其屬性的間接訪問脚猾,可以通過字符串來訪問一個(gè)對(duì)象的成員變量或其關(guān)聯(lián)的存取方法(getter or setter
)贸伐。
通常,我們可以直接通過存取方法或變量名來訪問對(duì)象的屬性觅赊。我們也可以使用 KVC 間接訪問對(duì)象的屬性桶唐,并且 KVC 還可以訪問私有變量。某些情況下 KVC 還可以幫助簡化代碼茉兰。
二、使用
1.訪問對(duì)象屬性
(1)常用 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 來賦值
(2)基礎(chǔ)操作
#import <Foundation/Foundation.h>
#import "DJTestModelOne.h"
@interface DJTestModel : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *test;
@property (nonatomic,strong) DJTestModelOne *one;
@end
#import <Foundation/Foundation.h>
@interface DJTestModelOne : NSObject
@property (nonatomic,copy)NSString *oneName;
@end
#import "ViewController.h"
#import "DJTestModel.h"
#import "DJTestModelOne.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
DJTestModel *model = [[DJTestModel alloc]init];
//直接賦值
[model setName:@"dj"];
//使用 KVC 賦值
[model setValue:@"dj" forKey:@"name"];
DJTestModelOne *oneModel = [[DJTestModelOne alloc]init];
model.one = oneModel;
//使用 KVC 的 keyPath 賦值
[model setValue:@"djOne" forKeyPath:@"one.oneName"];
//使用 KVC 取值
NSString *name = [model valueForKey:@"name"];
//使用 KVC 的 keyPath 取值
NSString *oneName = [model valueForKeyPath:@"one.oneName"];
}
@end
KVC 還支持多級(jí)訪問欣簇,KeyPath
用法跟點(diǎn)語法相同规脸。
(3)多值操作
給定一組 Key,獲得一組 value熊咽,以字典的形式返回莫鸭。該方法為數(shù)組中的每個(gè) Key 調(diào)用 valueForKey:
方法。
[model setValuesForKeysWithDictionary:@{@"name":@"djValue",@"test":@"testValue"}];
將指定字典中的值設(shè)置到消息接收者的屬性中横殴,使用字典的 Key 標(biāo)識(shí)屬性被因。默認(rèn)實(shí)現(xiàn)是為每個(gè)鍵值對(duì)調(diào)用 setValue:forKey:
方法 ,會(huì)根據(jù)需要用 nil
替換 NSNull
對(duì)象衫仑。
NSDictionary *dict = [model dictionaryWithValuesForKeys:@[@"name",@"test"]];
2.訪問集合屬性
我們可以像訪問其它對(duì)象一樣使用 valueForKey:
或 setValue:forKey:
方法來獲取或設(shè)置集合對(duì)象(主要指 NSArray
和 NSSet
)梨与。但是,當(dāng)我們要操作集合對(duì)象的內(nèi)容文狱,比如添加或者刪除元素時(shí)粥鞋,通過 KVC 的可變代理方法獲取集合代理對(duì)象是最有效的。
【擴(kuò)展:根據(jù) KVO 的實(shí)現(xiàn)原理瞄崇,是在運(yùn)行時(shí)動(dòng)態(tài)生成子類并重寫 setter
方法來達(dá)到可以通知所有觀察者對(duì)象的目的呻粹,因此我們對(duì)集合對(duì)象進(jìn)行操作是不會(huì)觸發(fā) KVO 的。當(dāng)我們要使用 KVO 監(jiān)聽集合對(duì)象變化時(shí)苏研,需要通過 KVC 的可變代理方法獲取集合代理對(duì)象等浊,然后對(duì)代理對(duì)象進(jìn)行操作。當(dāng)代理對(duì)象的內(nèi)部對(duì)象發(fā)生改變時(shí)摹蘑,會(huì)觸發(fā) KVO 的監(jiān)聽方法筹燕。】
KVC 提供了三種不同的代理對(duì)象訪問的代理方法衅鹿,每種都有 Key 和 KeyPath 兩種方法庄萎。
-
mutableArrayValueForKey:
和mutableArrayValueForKeyPath:
返回NSMutableArray
對(duì)象的代理對(duì)象。 -
mutableSetValueForKey:
和mutableSetValueForKeyPath:
返回NSMutableSet
對(duì)象的代理對(duì)象塘安。 -
mutableOrderedSetValueForKey:
和mutableOrderedSetValueForKeyPath:
返回NSMutableOrderedSet
對(duì)象的代理對(duì)象糠涛。
3.使用集合運(yùn)算符
KVC 的 valueForKeyPath:
方法除了可以取出屬性值以外,還可以在 KeyPath 中嵌套集合運(yùn)算符兼犯,來對(duì)集合對(duì)象進(jìn)行操作忍捡。
以下是KeyPath集合運(yùn)算符的格式集漾,主要分為 3 個(gè)部分:
- Left key path:左鍵路徑,要操作的集合對(duì)象砸脊,如果消息接收者就是集合對(duì)象具篇,則可以省略 Left 部分;
- Collection operator:集合運(yùn)算符凌埂;
- Right key path:右鍵路徑驱显,要進(jìn)行運(yùn)算的集合中的屬性。
集合運(yùn)算符主要分為三類:
- 聚合運(yùn)算符:以某種方式合并集合中的對(duì)象瞳抓,并返回右鍵路徑中指定的屬性的數(shù)據(jù)類型匹配的一個(gè)對(duì)象埃疫,一般返回
NSNumber
實(shí)例。 - 數(shù)組運(yùn)算符:根據(jù)運(yùn)算符的條件孩哑,將符合條件的對(duì)象以一個(gè)
NSArray
實(shí)例返回栓霜。 - 嵌套運(yùn)算符:處理集合對(duì)象中嵌套其他集合對(duì)象的情況,并根據(jù)運(yùn)算符返回一個(gè)
NSArray
或NSSet
實(shí)例横蜒。
示例:
#import <Foundation/Foundation.h>
#import "DJTestModelOne.h"
@interface DJTestModel : NSObject
@property (nonatomic,strong) NSArray <DJTestModelOne *> *oneArray;
@end
#import <Foundation/Foundation.h>
@interface DJTestModelOne : NSObject
@property (nonatomic,assign)NSInteger num;
@property (nonatomic,copy)NSString * type;
@end
(1)使用聚合運(yùn)算符
#import "ViewController.h"
#import "DJTestModel.h"
#import "DJTestModelOne.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
DJTestModel *model = [[DJTestModel alloc]init];
DJTestModelOne *one1 = [[DJTestModelOne alloc]init];
one1.num = 1;
DJTestModelOne *one2 = [[DJTestModelOne alloc]init];
one2.num = 2;
DJTestModelOne *one3 = [[DJTestModelOne alloc]init];
one3.num = 3;
DJTestModelOne *one4 = [[DJTestModelOne alloc]init];
one4.num = 4;
model.oneArray = @[one1,one2,one3,one4];
NSNumber *avg = [model.oneArray valueForKeyPath:@"@avg.num"];
NSNumber *conunt = [model.oneArray valueForKeyPath:@"@count"];
NSNumber *sum = [model.oneArray valueForKeyPath:@"@sum.num"];
NSNumber *max = [model.oneArray valueForKeyPath:@"@max.num"];
NSNumber *min = [model.oneArray valueForKeyPath:@"@min.num"];
}
@end
(2)使用數(shù)組運(yùn)算符
#import "ViewController.h"
#import "DJTestModel.h"
#import "DJTestModelOne.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
DJTestModel *model = [[DJTestModel alloc]init];
DJTestModelOne *one1 = [[DJTestModelOne alloc]init];
one1.type = @"one1";
DJTestModelOne *one2 = [[DJTestModelOne alloc]init];
one2.type = @"one2";
DJTestModelOne *one3 = [[DJTestModelOne alloc]init];
one3.type = @"one2";
DJTestModelOne *one4 = [[DJTestModelOne alloc]init];
one4.type = @"one4";
model.oneArray = @[one1,one2,one3,one4];
//獲取集合中的所有 type 對(duì)象
NSArray *types = [model.oneArray valueForKeyPath:@"@unionOfObjects.type"];
// 獲取集合中的所有不同的 type 對(duì)象
NSArray *disTypes = [model.oneArray valueForKeyPath:@"@distinctUnionOfObjects.type"];
}
在使用數(shù)組運(yùn)算符時(shí)胳蛮,如果有任何操作的對(duì)象為 nil
,則 valueForKeyPath:
方法將引發(fā)異常丛晌。
(3)使用嵌套運(yùn)算符
處理集合對(duì)象中嵌套其他集合對(duì)象的情況仅炊,并根據(jù)運(yùn)算符返回一個(gè)NSArray或NSSet實(shí)例。
NSArray *arrayOfArrays = @[model.oneArray,model.oneArray];
NSArray *arrayTypes = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.type"];
NSArray *distinctTypes = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.type"];
- 在使用嵌套運(yùn)算符時(shí)澎蛛,
valueForKeyPath:
內(nèi)部會(huì)根據(jù)運(yùn)算符創(chuàng)建一個(gè)NSMutableArray
或NSMutableSet
對(duì)象茂洒,將集合中的 array 和 set 添加進(jìn)去再進(jìn)行操作。如果集合中有非集合元素瓶竭,會(huì)導(dǎo)致 Crash督勺。 - 使用
unionOfArrays
或distinctUnionOfArrays
運(yùn)算符,消息接收者應(yīng)該是arrayOfArrays
類型斤贰,即NSArray< NSArray* >* arrayOfArrays;
使用distinctUnionOfSets
運(yùn)算符智哀,消息接收者應(yīng)該是setOfSets
或者arrayOfSets
類型。否則會(huì)發(fā)生異常荧恍。 - 在使用嵌套運(yùn)算符時(shí)瓷叫,如果有任何操作的對(duì)象為
nil
, 則valueForKeyPath:
方法將引發(fā)異常送巡。
(4)拓展
如果集合中的對(duì)象都是 NSNumber
摹菠,右鍵路徑可以用 self
。
NSArray *array = @[@1, @2, @3, @4, @5];
NSNumber *sum = [array valueForKeyPath:@"@sum.self"];
NSLog(@"%d",[sum intValue]);
4.非對(duì)象值處理
KVC 支持基礎(chǔ)數(shù)據(jù)類型和結(jié)構(gòu)體骗爆,在使用 KVC 進(jìn)行賦值或取值的時(shí)候次氨,會(huì)自動(dòng)在非對(duì)象值和對(duì)象值之間進(jìn)行轉(zhuǎn)換。
- 當(dāng)進(jìn)行取值如
valueForKey:
時(shí)摘投,如果返回值非對(duì)象煮寡,會(huì)使用該值初始化一個(gè)NSNumber
(用于基礎(chǔ)數(shù)據(jù)類型)或NSValue
(用于結(jié)構(gòu)體)實(shí)例虹蓄,然后返回該實(shí)例。 - 當(dāng)進(jìn)行賦值如
setValue:forKey:
時(shí)幸撕,如果 key 的數(shù)據(jù)類型非對(duì)象薇组,則會(huì)發(fā)送一條<type>Value
消息給 value 對(duì)象以提取基礎(chǔ)數(shù)據(jù),然后賦值給 key坐儿。
注意:因?yàn)镾wift 中的所有屬性都是對(duì)象律胀,所以這里僅適用于 Objective-C 屬性。
當(dāng)進(jìn)行賦值如 setValue:forKey:
時(shí)貌矿,如果 key 的數(shù)據(jù)類型是非對(duì)象類型炭菌,則 value 就禁止傳 nil
。否則會(huì)調(diào)用 setNilValueForKey:
方法站叼,該方法的默認(rèn)實(shí)現(xiàn)拋出異常 NSInvalidArgumentException
,并導(dǎo)致程序 Crash菇民。
5.屬性驗(yàn)證
KVC 提供了屬性驗(yàn)證的方法尽楔,如下。我們可以在使用 KVC 賦值前驗(yàn)證能否為這個(gè) key 賦值指定 value第练。
validateValue
方法的默認(rèn)實(shí)現(xiàn)是查看消息接收者類中是否實(shí)現(xiàn)了遵循命名規(guī)則為 validate<Key>:error:
的方法阔馋,如果有的話就返回調(diào)用該方法的結(jié)果;如果沒有的話娇掏,則默認(rèn)驗(yàn)證成功并返回 YES呕寝。我們可以在消息接收者類中實(shí)現(xiàn) validate<Key>:error:
的方法來自定義邏輯返回 YES 或 NO。
#import <Foundation/Foundation.h>
#import "DJTestModelOne.h"
@interface DJTestModel : NSObject
@property (nonatomic,strong) NSArray <DJTestModelOne *> *oneArray;
@property (nonatomic,copy)NSString *name;
@end
#import "DJTestModel.h"
@implementation DJTestModel
-(BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing _Nullable *)outError{
NSString *name = *ioValue;
BOOL result = NO;
if ([name isEqualToString:@"dj"]) {
result = YES;
}
return result;
}
@end
DJTestModel *model = [[DJTestModel alloc]init];
NSString *value = @"dj";
NSString *key = @"name";
NSError *error;
BOOL result = [model validateValue:&value forKey:key error:&error];
if (error) {
NSLog(@"error = %@", error);
}
三婴梧、原理
1.搜索規(guī)則
除了了解 KVC 的使用下梢,了解 KVC 取值和賦值過程的工作原理也是很有必要的。
(1)基本的 Getter 搜索模式
以下是 valueForKey:
方法的默認(rèn)實(shí)現(xiàn)塞蹭,給定一個(gè) key 作為輸入?yún)?shù)孽江,在消息接收者類中操作,執(zhí)行以下過程番电。
- 第一步:按照
get<Key>
岗屏、<key>
、is<Key>
漱办、_<key>
順序查找方法这刷。如果找到就調(diào)用取值并執(zhí)行第五步,否則執(zhí)行第二步娩井; - 第二步:查找
countOf<Key>
暇屋、objectIn<Key>AtIndex:
、<key>AtIndexes:
命名的方法洞辣。如果找到第一個(gè)和后面兩個(gè)中的至少一個(gè)率碾,則創(chuàng)建一個(gè)能夠響應(yīng)所有NSArray
的方法的集合代理對(duì)象(類型為NSKeyValueArray
叔营,繼承自NSArray
),并返回該對(duì)象所宰。否則執(zhí)行第三步绒尊;
代理對(duì)象隨后將其接收到的任何NSArray
消息轉(zhuǎn)換為countOf<Key>
、objectIn<Key>AtIndex:
仔粥、<Key>AtIndexes:
消息的組合婴谱,并將其發(fā)送給 KVC 調(diào)用方。如果原始對(duì)象還實(shí)現(xiàn)了一個(gè)名為get<Key>:range:
的可選方法躯泰,則代理對(duì)象也會(huì)在適當(dāng)時(shí)使用該方法谭羔。
當(dāng) KVC 調(diào)用方與代理對(duì)象一起工作時(shí),允許底層屬性的行為如同NSArray
一樣麦向,即使它不是NSArray
瘟裸。 - 第三步:查找
countOf<Key>
、enumeratorOf<Key>
诵竭、memberOf<Key>:
命名的方法话告。如果三個(gè)方法都找到,則創(chuàng)建一個(gè)能夠響應(yīng)所有NSSet
的方法的集合代理對(duì)象(類型為NSKeyValueSet
卵慰,繼承自NSSet
)沙郭,并返回該對(duì)象。否則執(zhí)行第四步裳朋;
代理對(duì)象隨后將其接收到的任何NSSet
消息轉(zhuǎn)換為countOf<Key>
病线、enumeratorOf<Key>
、memberOf<Key>:
消息的組合鲤嫡,并將其發(fā)送給 KVC 調(diào)用方送挑。
當(dāng) KVC 調(diào)用方與代理對(duì)象一起工作時(shí),允許底層屬性的行為如同NSSet
一樣暖眼,即使它不是NSSet
让虐。 - 第四步:查看消息接收者類的
+accessInstanceVariablesDirectly
方法的返回值(默認(rèn)返回YES
)。如果返回YES
罢荡,就按照_<key>
赡突、_is<Key>
、<key>
区赵、is<Key>
順序查找成員變量惭缰。如果找到就直接取值并執(zhí)行第五步,否則執(zhí)行第六步笼才。如果+accessInstanceVariablesDirectly
方法返回NO
也執(zhí)行第六步漱受。 - 第五步:如果取到的值是一個(gè)對(duì)象指針,即獲取的是對(duì)象,則直接將對(duì)象返回昂羡。如果取到的值是一個(gè)
NSNumber
支持的數(shù)據(jù)類型絮记,則將其存儲(chǔ)在NSNumber
實(shí)例并返回。如果取到的值不是一個(gè)NSNumber
支持的數(shù)據(jù)類型虐先,則轉(zhuǎn)換為NSValue
對(duì)象, 然后返回怨愤。 - 第六步:調(diào)用
valueForUndefinedKey:
方法,該方法拋出異常NSUnknownKeyException
蛹批,并導(dǎo)致程序 Crash撰洗。這是默認(rèn)實(shí)現(xiàn),我們可以重寫該方法根據(jù)特定 key 做一些特殊處理腐芍。
(2)基本的 Setter 搜索模式
以下是 setValue:forKey:
方法的默認(rèn)實(shí)現(xiàn)差导,給定 key 和 value 作為輸入?yún)?shù),嘗試將 KVC 調(diào)用方的屬性名為 ke y的值設(shè)置為 value猪勇,執(zhí)行以下過程设褐。
- 第一步:按照
set<Key>:
、_set<Key>:
順序查找方法泣刹。如果找到就調(diào)用并將 value 傳進(jìn)去(根據(jù)需要進(jìn)行數(shù)據(jù)類型轉(zhuǎn)換)助析,否則執(zhí)行第二步。 - 第二步:查看消息接收者類的
+accessInstanceVariablesDirectly
方法的返回值(默認(rèn)返回YES
)项玛。如果返回YES
貌笨,就按照_<key>
弱判、_is<Key>
襟沮、<key>
、is<Key>
順序查找成員變量(同基本的 Getter 搜索模式)昌腰。如果找到就將 value 賦值給它(根據(jù)需要進(jìn)行數(shù)據(jù)類型轉(zhuǎn)換)开伏,否則執(zhí)行第三步。如果+accessInstanceVariablesDirectly
方法返回NO
也執(zhí)行第三步遭商。 - 第三步:調(diào)用
setValue:forUndefinedKey:
方法固灵,該方法拋出異常NSUnknownKeyException
,并導(dǎo)致程序 Crash劫流。這是默認(rèn)實(shí)現(xiàn)巫玻,我們可以重寫該方法根據(jù)特定 key 做一些特殊處理。
2.異常處理
根據(jù) KVC 搜索規(guī)則祠汇,當(dāng)沒有搜索到對(duì)應(yīng)的 key 或者 keyPath 相關(guān)方法或者變量時(shí)仍秤,會(huì)調(diào)用對(duì)應(yīng)的異常方法 valueForUndefinedKey:
或 setValue:forUndefinedKey:
,這兩個(gè)方法的默認(rèn)實(shí)現(xiàn)是拋出異常 NSUnknownKeyException
可很,并導(dǎo)致程序 Crash诗力。我們可以重寫這兩個(gè)方法來處理異常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
當(dāng)進(jìn)行賦值如 setValue:forKey:
時(shí)我抠,如果 key 的數(shù)據(jù)類型是非對(duì)象類型苇本,則 value 就禁止傳 nil
袜茧。否則會(huì)調(diào)用 setNilValueForKey:
方法,該方法的默認(rèn)實(shí)現(xiàn)是拋出異常 NSInvalidArgumentException
瓣窄,并導(dǎo)致程序 Crash笛厦。我們可以重寫這個(gè)方法來處理異常。
- (void)setNilValueForKey:(NSString *)key
{
if ([key isEqualToString:@"hidden"]) {
[self setValue:@(NO) forKey:@”hidden”];
} else {
[super setNilValueForKey:key];
}
}