KVC 全名:Key-value coding,中文簡(jiǎn)直編碼。蘋果對(duì)其定義如下:
Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. When an object is key-value coding compliant, its properties are addressable via string parameters through a concise, uniform messaging interface. This indirect access mechanism supplements the direct access afforded by instance variables and their associated accessor methods.
鍵-值編碼是由NSKeyValueCoding非正式協(xié)議啟用的一種機(jī)制,對(duì)象采用該協(xié)議來提供對(duì)其屬性的間接訪問。當(dāng)對(duì)象符合鍵值編碼時(shí),可以通過簡(jiǎn)潔、統(tǒng)一的消息傳遞接口通過字符串參數(shù)對(duì)其屬性進(jìn)行尋址。這種間接訪問機(jī)制補(bǔ)充了實(shí)例變量及其關(guān)聯(lián)訪問器方法提供的直接訪問。
一、使用場(chǎng)景
KVC在我們的平常開發(fā)中應(yīng)該是高頻使用的,接下來我們先簡(jiǎn)單看一下常用的KVC使用場(chǎng)景:
1.基本類型
LPPerson *person = [[LGPerson alloc] init];
// 一般setter 方法
person.name = @"name"; // setter -- llvm
person.age = 18;
person->myName = @"name";
NSLog(@"%@ - %d - %@",person.name,person.age,person->myName);
// 非正式協(xié)議 - 間接訪問
[person setValue:@"KC" forKey:@"name"];
2.集合類型
person.array = @[@"1",@"2",@"3"];
// 修改數(shù)組
// person.array[0] = @"100";
// 第一種:搞一個(gè)新的數(shù)組 - KVC 賦值就OK
NSArray *array = [person valueForKey:@"array"];
array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
NSLog(@"%@",[person valueForKey:@"array"]);
// 第二種
NSMutableArray *mArray = [person mutableArrayValueForKey:@"array"];
mArray[0] = @"200";
NSLog(@"%@",[person valueForKey:@"array"]);
3.訪問非對(duì)象屬性
typedef struct {
float x, y, z;
} ThreeFloats;
ThreeFloats floats = {1.,2.,3.};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *value1 = [person valueForKey:@"threeFloats"];
NSLog(@"%@",value1);
ThreeFloats th;
[value1 getValue:&th];
NSLog(@"%f-%f-%f",th.x,th.y,th.z);
4.key-path
LPStudent *student = [LPStudent alloc];
student.subject = @"火箭班";
person.student = student;
[person setValue:@"Swift" forKeyPath:@"student.subject"];
NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);
二、KVC原理
既然要了解KVC的原理,那么蘋果官方的文檔必定是不二之選,[傳送門]。(https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueCoding/SearchImplementation.html#//apple_ref/doc/uid/20000955-CJBBBFFA)
我們先看下setter和getter:
1.setter
Search Pattern for the Basic Setter
The default implementation of setValue:forKey:, given key and value parameters as input, attempts to set a property named key to value (or, for non-object properties, the unwrapped version of value, as described in Representing Non-Object Values) inside the object receiving the call, using the following procedure:
1.Look for the first accessor named set<Key>: or _set<Key>, in that order. If found, invoke it with the input value (or unwrapped value, as needed) and finish.
2.If no simple accessor is found, and if the class method accessInstanceVariablesDirectly returns YES, look for an instance variable with a name like _<key>, _is<Key>, <key>, or is<Key>, in that order. If found, set the variable directly with the input value (or unwrapped value) and finish.
3.Upon finding no accessor or instance variable, invoke setValue:forUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.
搜索基本Setter的模式
setValue:forKey:的默認(rèn)實(shí)現(xiàn),給定鍵和值參數(shù)作為輸入,嘗試在接收調(diào)用的對(duì)象內(nèi)部設(shè)置一個(gè)名為key的屬性為value(或者,對(duì)于非對(duì)象屬性,為value的解包裝版本,如描述的非對(duì)象值),使用以下過程:
1.按照順序查找第一個(gè)訪問器set<Key>:或_set<Key>。如果找到,使用輸入值(或根據(jù)需要打開包裝值)和finish調(diào)用它。
2.如果沒有找到簡(jiǎn)單的訪問器,并且類方法accessinstancevariables直接返回YES,那么查找一個(gè)實(shí)例變量,其名稱如下:_<key>,_is< key>,<key>,或is< key>。如果找到,直接用輸入值(或打開包裝的值)和finish設(shè)置變量。
3.在沒有找到訪問器或?qū)嵗兞繒r(shí),調(diào)用setValue:forUndefinedKey:。這在默認(rèn)情況下會(huì)引發(fā)一個(gè)異常,但是NSObject的一個(gè)子類可能提供特定于鍵的行為。
舉個(gè)例子:比如我們要給LPPerson的name賦值,首先會(huì)去找_name,如果沒有沒有找到,就會(huì)找_isName,還是沒有找name,最后再找isName。如果找到了就直接設(shè)值,沒有找到就會(huì)調(diào)用setValue:forUndefinedKey,來拋出異常。
2.getter
Search Pattern for the Basic Getter
The default implementation of valueForKey:, given a key parameter as input, carries out the following procedure, operating from within the class instance receiving the valueForKey: call.
1.Search the instance for the first accessor method found with a name like get<Key>, <key>, is<Key>, or _<key>, in that order. If found, invoke it and proceed to step 5 with the result. Otherwise proceed to the next step.
2.If no simple accessor method is found, search the instance for methods whose names match the patterns countOf<Key> and objectIn<Key>AtIndex: (corresponding to the primitive methods defined by the NSArray class) and <key>AtIndexes: (corresponding to the NSArray method objectsAtIndexes:).
If the first of these and at least one of the other two is found, create a collection proxy object that responds to all NSArray methods and return that. Otherwise, proceed to step 3.
The proxy object subsequently converts any NSArray messages it receives to some combination of countOf<Key>, objectIn<Key>AtIndex:, and <key>AtIndexes: messages to the key-value coding compliant object that created it. If the original object also implements an optional method with a name like get<Key>:range:, the proxy object uses that as well, when appropriate. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSArray, even if it is not.
3.If no simple accessor method or group of array access methods is found, look for a triple of methods named countOf<Key>, enumeratorOf<Key>, and memberOf<Key>: (corresponding to the primitive methods defined by the NSSet class).
If all three methods are found, create a collection proxy object that responds to all NSSet methods and return that. Otherwise, proceed to step 4.
This proxy object subsequently converts any NSSet message it receives into some combination of countOf<Key>, enumeratorOf<Key>, and memberOf<Key>: messages to the object that created it. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSSet, even if it is not.
4.If no simple accessor method or group of collection access methods is found, and if the receiver's class method accessInstanceVariablesDirectly returns YES, search for an instance variable named _<key>, _is<Key>, <key>, or is<Key>, in that order. If found, directly obtain the value of the instance variable and proceed to step 5. Otherwise, proceed to step 6.
5.If the retrieved property value is an object pointer, simply return the result.
If the value is a scalar type supported by NSNumber, store it in an NSNumber instance and return that.
If the result is a scalar type not supported by NSNumber, convert to an NSValue object and return that.
6.If all else fails, invoke valueForUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.
基本Getter的搜索模式
valueForKey:的默認(rèn)實(shí)現(xiàn),給定一個(gè)key參數(shù)作為輸入,執(zhí)行以下過程,從接收valueForKey:調(diào)用的類實(shí)例內(nèi)部進(jìn)行操作。
1.在實(shí)例中搜索找到的第一個(gè)訪問器方法,其名稱如下:get<Key>,<Key>,is<Key>,或_< Key>。如果找到了,就調(diào)用它,并在步驟5中處理結(jié)果。否則繼續(xù)下一步。
2.如果沒有找到簡(jiǎn)單的訪問器方法,則在實(shí)例中搜索名稱與模式countOf<Key>和objectIn<Key>AtIndex:(對(duì)應(yīng)于NSArray類定義的基本方法)和<Key> AtIndexes:(對(duì)應(yīng)于NSArray方法objectsAtIndexes:)匹配的方法。
如果找到第一個(gè)和至少兩個(gè)中的一個(gè),則創(chuàng)建一個(gè)集合代理對(duì)象,該對(duì)象響應(yīng)所有NSArray方法并返回該方法。否則,繼續(xù)執(zhí)行步驟3。
代理對(duì)象隨后將接收到的任何NSArray消息轉(zhuǎn)換為countOf<Key>、objectIn<Key>AtIndex:和<Key> AtIndexes:的一些組合,這些組合將消息發(fā)送給創(chuàng)建它的符合鍵值編碼的對(duì)象。如果原始對(duì)象還實(shí)現(xiàn)了一個(gè)名為get<Key>:range:的可選方法,代理對(duì)象也會(huì)在適當(dāng)?shù)臅r(shí)候使用它。實(shí)際上,代理對(duì)象與與鍵值編碼兼容的對(duì)象一起工作,允許底層屬性像NSArray一樣工作,即使它不是NSArray。
3.如果沒有找到簡(jiǎn)單的訪問方法或數(shù)組訪問方法組,查找三個(gè)方法,分別為countOf<Key>,enumeratorOf<Key>,memberOf<Key>:(對(duì)應(yīng)于NSSet類定義的原語方法)。
如果這三個(gè)方法都找到了,創(chuàng)建一個(gè)集合代理對(duì)象,它響應(yīng)所有NSSet方法并返回那個(gè)。否則,繼續(xù)執(zhí)行步驟4。
這個(gè)代理對(duì)象隨后將它接收到的任何NSSet消息轉(zhuǎn)換為countOf<Key>、enumeratorOf<Key>和memberOf<Key>:消息的組合,并發(fā)送給創(chuàng)建它的對(duì)象。實(shí)際上,代理對(duì)象與遵循鍵值編碼的對(duì)象一起工作,允許底層屬性像NSSet一樣運(yùn)行,即使它不是NSSet。
4.如果沒有找到簡(jiǎn)單的訪問方法或集合訪問方法組,并且如果接收方的類方法accessinstancevariables直接返回YES,那么按照順序搜索一個(gè)實(shí)例變量:_<key>,_is< key>,<key>,或is< key>。如果找到,直接獲取實(shí)例變量的值并繼續(xù)執(zhí)行步驟5。否則,繼續(xù)執(zhí)行步驟6。
5.如果檢索到的屬性值是一個(gè)對(duì)象指針,只需返回結(jié)果。
如果值是NSNumber支持的標(biāo)量類型,將其存儲(chǔ)在NSNumber實(shí)例中并返回。
如果結(jié)果是NSNumber不支持的標(biāo)量類型,轉(zhuǎn)換為NSValue對(duì)象并返回它。
6.如果所有其他方法都失敗,則調(diào)用valueForUndefinedKey:。這在默認(rèn)情況下會(huì)引發(fā)一個(gè)異常,但是NSObject的一個(gè)子類可能提供特定于鍵的行為。
和setter類似,比如我們要取LPPerson的name賦值,首先會(huì)去找getName,如果沒有沒有找到,就會(huì)找name,還是沒有找isName,再找_name。如果找到了就直接設(shè)值,沒有找到就會(huì)去實(shí)例中搜索名稱與模式。
接下來我們通過一個(gè)demo演示一下:
新建LPPerson對(duì)象:
@interface LPPerson : NSObject{
@public
NSString *_isName;
NSString *name;
NSString *isName;
NSString *_name;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
@implementation LPPerson
#pragma mark - 關(guān)閉或開啟實(shí)例變量賦值
+ (BOOL)accessInstanceVariablesDirectly{
return YES;
}
//MARK: - setKey. 的流程分析
- (void)setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)_setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)_setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
//MARK: - valueForKey 流程分析 - get<Key>, <key>, is<Key>, or _<key>,
- (NSString *)getName{
return NSStringFromSelector(_cmd);
}
- (NSString *)name{
return NSStringFromSelector(_cmd);
}
- (NSString *)isName{
return NSStringFromSelector(_cmd);
}
- (NSString *)_name{
return NSStringFromSelector(_cmd);
}
@end
我們?cè)?code>LPPerson中創(chuàng)建了4個(gè)成員變量:_isName , name, isName, _name,是用來測(cè)試文檔說的當(dāng)accessinstancevariables為YES時(shí),會(huì)訪問成員變量。
在Viewcontroller中實(shí)現(xiàn)如下代碼:
- (void)viewDidLoad {
[super viewDidLoad];
LPPerson *person = [[LPPerson alloc] init];
// 1: KVC - 設(shè)置值的過程 setValue 分析調(diào)用過程
[person setValue:@"peter" forKey:@"name"];
}
我們運(yùn)行工程,查看打印結(jié)果:
2020-10-31 10:50:55.580747+0800 002-KVC取值&賦值過程[33506:1118311] -[LPPerson setName:] - peter
可以看到執(zhí)行了setName方法,我們把setName注釋掉,再次運(yùn)行:
2020-10-31 10:52:44.333568+0800 002-KVC取值&賦值過程[33532:1120381] -[LPPerson _setName:] - peter
結(jié)果是執(zhí)行了_setName方法,我們把_setName也注釋掉,再運(yùn)行:
2020-10-31 10:53:52.679457+0800 002-KVC取值&賦值過程[33548:1121542] -[LPPerson setIsName:] - peter
結(jié)果是執(zhí)行了setIsName方法,我們把setIsName也注釋掉,再運(yùn)行:
這次沒有執(zhí)行_setIsName方法。
按照文檔第二步,說如果沒有找到簡(jiǎn)單的訪問器,并且accessinstancevariables為YES時(shí),回去查找實(shí)例變量,接下來我們來再次驗(yàn)證下,在viewDidLoad中代碼修改為如下:
- (void)viewDidLoad {
[super viewDidLoad];
LPPerson *person = [[LPPerson alloc] init];
// 1: KVC - 設(shè)置值的過程 setValue 分析調(diào)用過程
[person setValue:@"peter" forKey:@"name"];
NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
}
執(zhí)行,觀察運(yùn)行結(jié)果:
2020-10-31 11:01:07.472392+0800 002-KVC取值&賦值過程[33622:1127379] peter-(null)-(null)-(null)
說明是set的_name中的值,我們?cè)傩薷囊幌麓a:
@interface LPPerson : NSObject{
@public
NSString *_isName;
NSString *name;
NSString *isName;
// NSString *_name;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
// NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
再次運(yùn)行:
2020-10-31 13:39:29.052301+0800 002-KVC取值&賦值過程[34763:1208784] peter-(null)-(null)
說明是set的_isName中的值,我們?cè)傩薷囊幌麓a:
@interface LPPerson : NSObject{
@public
// NSString *_isName;
NSString *name;
NSString *isName;
// NSString *_name;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
// NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
// NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
NSLog(@"%@-%@",person->name,person->isName);
運(yùn)行,查看結(jié)果:
2020-10-31 13:40:58.689786+0800 002-KVC取值&賦值過程[34789:1210795] peter-(null)
說明是set的name的值,我們?cè)傩薷南拢?/p>
@interface LPPerson : NSObject{
@public
// NSString *_isName;
// NSString *name;
NSString *isName;
// NSString *_name;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
// NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
// NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
// NSLog(@"%@-%@",person->name,person->isName);
NSLog(@"%@",person->isName);
再運(yùn)行:
2020-10-31 13:42:36.330874+0800 002-KVC取值&賦值過程[34813:1212798] peter
可以看到,也是可以的。
這個(gè)時(shí)候,我們把LPPerson的accessInstanceVariablesDirectly置為NO,再運(yùn)行下呢:
+ (BOOL)accessInstanceVariablesDirectly{
return NO;
}

可以看到,直接奔潰了。
上述的結(jié)果就可以驗(yàn)證到文檔說的set過程。
接下來,我們驗(yàn)證下get的過程:
先將LPPerson的成員變量恢復(fù):
@interface LPPerson : NSObject{
@public
NSString *_isName;
NSString *name;
NSString *isName;
NSString *_name;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
再在viewDidLoad中添加代碼:
- (void)viewDidLoad {
[super viewDidLoad];
LPPerson *person = [[LPPerson alloc] init];
// 1: KVC - 設(shè)置值的過程 setValue 分析調(diào)用過程
[person setValue:@"peter" forKey:@"name"];
// setter - getter - KVC 設(shè)值 和 取值的流程
// 2: KVC - 取值的過程
NSLog(@"取值:%@",[person valueForKey:@"name"]);
}
運(yùn)行查看結(jié)果:
2020-10-31 13:48:13.369199+0800 002-KVC取值&賦值過程[34884:1217744] 取值:getName
走到了getName方法,我們注釋掉getName方法,再次運(yùn)行:
2020-10-31 13:50:46.767696+0800 002-KVC取值&賦值過程[34911:1220210] 取值:name
可以看到執(zhí)行了name方法,我們注釋掉name方法,再次運(yùn)行:
2020-10-31 13:51:21.653834+0800 002-KVC取值&賦值過程[34924:1221160] 取值:isName
執(zhí)行了isName方法,我們?cè)侔?code>isName注釋掉,再運(yùn)行:
2020-10-31 13:51:59.197860+0800 002-KVC取值&賦值過程[34937:1222157] 取值:_name
也執(zhí)行了_name方法
同樣的,我們把_name注釋掉,并且設(shè)置accessinstancevariables返回YES,再次修改下ViewDidLoad方法:
- (void)viewDidLoad {
[super viewDidLoad];
LPPerson *person = [[LPPerson alloc] init];
// 1: KVC - 設(shè)置值的過程 setValue 分析調(diào)用過程
[person setValue:@"peter" forKey:@"name"];
// setter - getter - KVC 設(shè)值 和 取值的流程
// 2: KVC - 取值的過程
person->_name = @"_name = peter";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
}
再次運(yùn)行:
2020-10-31 13:55:49.859847+0800 002-KVC取值&賦值過程[34997:1226759] 取值:_name = peter
再修改成_isName:
@interface LPPerson : NSObject{
@public
NSString *_isName;
NSString *name;
NSString *isName;
// NSString *_name;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
- (void)viewDidLoad {
[super viewDidLoad];
LPPerson *person = [[LPPerson alloc] init];
// 1: KVC - 設(shè)置值的過程 setValue 分析調(diào)用過程
[person setValue:@"peter" forKey:@"name"];
// setter - getter - KVC 設(shè)值 和 取值的流程
// 2: KVC - 取值的過程
// person->_name = @"_name = peter";
person->_isName = @"_isName = peter";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
}
再運(yùn)行:
2020-10-31 13:57:15.787149+0800 002-KVC取值&賦值過程[35027:1229056] 取值:_isName = peter
在修改為name:
@interface LPPerson : NSObject{
@public
// NSString *_isName;
NSString *name;
NSString *isName;
// NSString *_name;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
- (void)viewDidLoad {
[super viewDidLoad];
LPPerson *person = [[LPPerson alloc] init];
// 1: KVC - 設(shè)置值的過程 setValue 分析調(diào)用過程
[person setValue:@"peter" forKey:@"name"];
// setter - getter - KVC 設(shè)值 和 取值的流程
// 2: KVC - 取值的過程
// person->_name = @"_name = peter";
// person->_isName = @"_isName = peter";
person->name = @"name = peter";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
}
運(yùn)行:
2020-10-31 13:58:42.396675+0800 002-KVC取值&賦值過程[35052:1231112] 取值:name = peter
最后,我們?cè)傩薷臑?code>isName:
@interface LPPerson : NSObject{
@public
// NSString *_isName;
// NSString *name;
NSString *isName;
// NSString *_name;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
@end
- (void)viewDidLoad {
[super viewDidLoad];
LPPerson *person = [[LPPerson alloc] init];
// 1: KVC - 設(shè)置值的過程 setValue 分析調(diào)用過程
[person setValue:@"peter" forKey:@"name"];
// setter - getter - KVC 設(shè)值 和 取值的流程
// 2: KVC - 取值的過程
// person->_name = @"_name = peter";
// person->_isName = @"_isName = peter";
// person->name = @"name = peter";
person->isName = @"isName = peter";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
}
運(yùn)行:
2020-10-31 13:59:52.374174+0800 002-KVC取值&賦值過程[35077:1232750] 取值:isName = peter
我們?cè)侔?code>accessinstancevariables設(shè)置為NO試試:
+ (BOOL)accessInstanceVariablesDirectly{
return NO;
}

同樣奔潰了,上述結(jié)果都驗(yàn)證了文檔的過程。
所以可見文檔的重要性,我們一定要要成良好的查閱文檔的習(xí)慣,可以幫助我們正確的理解。