KVC定义
Key-value coding is a mechanism enabled by the
`NSKeyValueCoding` informal protocol that objects adopt to
provide indirect access to their properties.
简单的说就是允许开发者通过Key名直接访问对象的属性(取值,赋值)。而不需要调用
存取方法。这样就可以在运行时动态地读取或修改对象的属性。
KVC赋值流程
-
文档说明
The default implementation of the
NSKeyValueCodingprotocol provided byNSObjectmaps key-based accessor calls to an object’s underlying properties using a clearly defined set of rules. These protocol methods use a key parameter to search their own object instance for accessors, instance variables, and related methods that follow certain naming conventions. Although you rarely modify this default search, it can be helpful to understand how it works, both for tracing the behavior of key-value coded objects, and for making your own objects compliant.Search Pattern for the Basic Setter
The default implementation of
setValue:forKey:, givenkeyandvalueparameters as input, attempts to set a property namedkeytovalue(or, for non-object properties, the unwrapped version ofvalue, as described in Representing Non-Object Values) inside the object receiving the call, using the following procedure:- 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. - If no simple accessor is found, and if the class method
accessInstanceVariablesDirectlyreturnsYES, look for an instance variable with a name like_<key>,_is<Key>,<key>, oris<Key>, in that order. If found, set the variable directly with the input value (or unwrapped value) and finish. - Upon finding no accessor or instance variable, invoke
setValue:forUndefinedKey:. This raises an exception by default, but a subclass ofNSObjectmay provide key-specific behavior.
- Look for the first accessor named
- 基于GNUStep源码整理。整体调用栈如下:
// 源码在base/Source/Fundation/NSKeyValueCoding.m
- (void) setValue: (id)anObject forKey: (NSString*)aKey {
SetValueForKey(self, anObject, key, size);
}
//获取key对应的setter sel,或者key对应的变量类型以及变量偏移量offset
SetValueForKey(self, anObject, key, size) {
GSObjCSetVal(self, key, anObject, sel, type, size, off);
}
// GSObjCSetVal()实现在 base/Source/Additions/GSObjCRuntime.m
具体实现:
static void SetValueForKey(NSObject *self, id anObject, const char *key, unsigned size)
{
SEL sel = 0;
const char *type = 0;
int off = 0;
if (size > 0) {
const char *name;
char buf[size + 6];//额外需要_set,':','\0' 这6个字符
char lo;
char hi;
memcpy(buf, "_set", 4);
memcpy(&buf[4], key, size);
lo = buf[4];
hi = islower(lo) ? toupper(lo) : lo;
buf[4] = hi;
buf[size + 4] = ':';
buf[size + 5] = '\0';
name = &buf[1]; // setKey:
type = NULL;
sel = sel_getUid(name);// sel == setKey:
if (sel == 0 || [self respondsToSelector: sel] == NO) {
// 如果不响应setKey方法,尝试带下划线的_setKey方法
name = buf; // _setKey:
sel = sel_getUid(name); // sel == _setKey:
if (sel == 0 || [self respondsToSelector: sel] == NO) {
// 如果不响应_setKey方法
sel = 0;// 这里置空
if ([[self class] accessInstanceVariablesDirectly] == YES) {
buf[size + 4] = '\0';
buf[3] = '_';
buf[4] = lo;
name = &buf[3]; // _key
/*
查找对应变量var 的offset,赋值是直接同self + var_offset 进行赋值的
也就是self + offset 就是var所在的内存空间
*/
if (GSObjCFindVariable(self, name, &type, &size, &off) == NO)
{
buf[4] = hi;
buf[3] = 's';
buf[2] = 'i';
buf[1] = '_';
name = &buf[1]; // _isKey
if (GSObjCFindVariable(self, name, &type, &size, &off) == NO) {
buf[4] = lo;
name = &buf[4]; // key
if (GSObjCFindVariable(self, name, &type, &size, &off) == NO) {
buf[4] = hi;
buf[3] = 's';
buf[2] = 'i';
name = &buf[2]; // isKey
GSObjCFindVariable(self, name, &type, &size, &off);
}
}
}
}
}else{
GSOnceFLog(@"Key-value access using _setKey: is deprecated:");
}
}
}
GSObjCSetVal(self, key, anObject, sel, type, size, off);
}
//GSObjCSetVal()代码太多只截取一种类型的赋值代码
GSObjCSetVal(NSObject *self, const char *key, id val, SEL sel,
const char *type, unsigned size, int offset)
{
static NSNull *null = nil;
NSMethodSignature *sig = nil;
if (null == nil){
null = [NSNull new];
}
if (sel != 0){
sig = [self methodSignatureForSelector: sel];
if ([sig numberOfArguments] != 3){
[NSException raise: NSInvalidArgumentException
format: @"key-value set method has wrong number of args"];
}
type = [sig getArgumentTypeAtIndex: 2];
}
if (type == NULL){
[self setValue: val forUndefinedKey:[NSString stringWithUTF8String: key]];
}else if ((val == nil || val == null) && *type != _C_ID && *type != _C_CLASS){
[self setNilValueForKey: [NSString stringWithUTF8String: key]];
}else{
switch (*type)
{
case _C_CHR:
{
char v = [val charValue];
if (sel == 0){
char *ptr = (char *)((char *)self + offset);
*ptr = v;
}else{
void (*imp)(id, SEL, char) = (void (*)(id, SEL, char))[self methodForSelector: sel];
(*imp)(self, sel, v);
}
}
break;
default:
[self setValue: val forUndefinedKey:[NSString stringWithUTF8String: key]];
}
}
}
-
从上面的源码可以看出SetValueForKey方法主要是获取key对应的sel(setKey或者_setKey),或者key对应的变量类型以及变量偏移量(变量名可能是_key, _isKey, key, isKey)
获取key对应的变量类型以及变量偏移量前先通过accessInstanceVariablesDirectly 判断是否可以直接访问变量,如果可以,才按照变量名为(_key,_isKey,key,isKey) 的顺序查询变量类型以及变量偏移量
-
获取sel,变量的类型以及偏移量结束后,调用GSObjCSetVal进行最终的赋值操作。
在GSObjCSetVal中判断如果sel不为空,则获取sel的方法签名,然后从方法签名中获取 变量类型,
-
如果在SetValueForKey中既获取不到sel,也获取不到变量类型,那么在GSObjCSetVal中 就会调用
[self setValue: val forUndefinedKey:[NSString stringWithUTF8String: key]];后直接返回
-
如果sel获取到,那么就拿到sel对应的IMP直接调用进行赋值,
如果sel获取不到,但获取到了变量类型及偏移量,那么就通过
id *ptr = (id *)((char *)self + offset); ASSIGN(*ptr, v);的方式获取到变量的内存地址,然后赋值
KVC取值流程
*苹果文档说明
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.
-
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. -
If no simple accessor method is found, search the instance for methods whose names match the patterns
countOf<Key>andobjectIn<Key>AtIndex:(corresponding to the primitive methods defined by theNSArrayclass) and<key>AtIndexes:(corresponding to theNSArraymethodobjectsAtIndexes:).If the first of these and at least one of the other two is found, create a collection proxy object that responds to all
NSArraymethods and return that. Otherwise, proceed to step 3.The proxy object subsequently converts any
NSArraymessages it receives to some combination ofcountOf<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 likeget<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 anNSArray, even if it is not. -
If no simple accessor method or group of array access methods is found, look for a triple of methods named
countOf<Key>,enumeratorOf<Key>, andmemberOf<Key>:(corresponding to the primitive methods defined by theNSSetclass).If all three methods are found, create a collection proxy object that responds to all
NSSetmethods and return that. Otherwise, proceed to step 4.This proxy object subsequently converts any
NSSetmessage it receives into some combination ofcountOf<Key>,enumeratorOf<Key>, andmemberOf<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 anNSSet, even if it is not. -
If no simple accessor method or group of collection access methods is found, and if the receiver's class method
accessInstanceVariablesDirectlyreturnsYES, search for an instance variable named_<key>,_is<Key>,<key>, oris<Key>, in that order. If found, directly obtain the value of the instance variable and proceed to step 5. Otherwise, proceed to step 6. -
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 anNSNumberinstance and return that.If the result is a scalar type not supported by NSNumber, convert to an
NSValueobject and return that. -
If all else fails, invoke
valueForUndefinedKey:. This raises an exception by default, but a subclass ofNSObjectmay provide key-specific behavior.
翻译过来就是:
1. 按顺序查找名称为get<Key>、<key>、is<Key>或_<key>的方法。
2. 查找名称如countOf<Key>、objectIn<Key>AtIndex:、<key>AtIndexes:的方法。
3. 查找名称如countOf<Key>,enumeratorOf<Key>, 和memberOf<Key>:方法。
4. 如果类方法accessInstanceVariablesDirectly返回YES,则按顺序查找名称为_<key>、_is<Key>、<key>或is<Key>的实例变量。
5. 分为如下三种情况:
a. 如果返回的属性值是对象的指针,则直接返回结果。
b. 如果返回的属性值是`NSNumber`支持的基础数据类型,则将其存储在 `NSNumber`实例中并返回该值。
c. 如果返回的属性值是`NSNumber`不支持的数据类型,则转换为`NSValue`对象并返回该对象。
6. 如果上述步骤都失败了,调用`valueForUndefinedKey:`,该方法默认抛出异常`NSUndefinedKeyException`。
KVC需要注意的地方
-
value 不能为空,否则就会出现异常,官方文档内容如下
In the default implementation, when you attempt to set a non-object property to a `nil` value, the key-value coding compliant object sends itself a `setNilValueForKey:`message. The default implementation of `setNilValueForKey:` raises an `NSInvalidArgumentException`, but an object may override this behavior to substitute a default value or a marker value instead,翻译过来就是说,如果value是nil时,底层会发送一条
[self setNilValueForKey:key]消息,setNilValueForKey默认实现会抛出一个NSInvalidArgumentException异常,子类可以重载该方法提供默认值。具体源码如下:- (void) setNilValueForKey: (NSString*)aKey { #ifdef WANT_DEPRECATED_KVC_COMPAT static IMP o = 0; /* Backward compatibility hack */ if (o == 0){ o = [NSObject instanceMethodForSelector:@selector(unableToSetNilForKey:)]; } if ([self methodForSelector: @selector(unableToSetNilForKey:)] != o){ [self unableToSetNilForKey: aKey]; return; } #endif [NSException raise: NSInvalidArgumentException format: @"%@ -- %@ 0x%"PRIxPTR": Given nil value to set for key \"%@\"", NSStringFromSelector(_cmd), NSStringFromClass([self class]), (NSUInteger)self, aKey]; } -
key, keyPath需要正确,否则就会出现异常,官方文档内容如下
If the specified key corresponds to a property that the object receiving the setter call does not have, the object sends itself a `setValue:forUndefinedKey:` message. The default implementation of `setValue:forUndefinedKey:` raises an `NSUndefinedKeyException`. However, subclasses may override this method to handle the request in a custom manner.翻译过来就是如果指定的键对应于接收setter调用的对象的属性如果没有, 对象会给自己发送一个
setValue:forUndefinedKey:消息。默认的setValue:forUndefinedKey:实现会引发'NSUnknownKeyException'。子类可以重载该方法处理这种情况源码如下
- (void) setValue: (id)anObject forUndefinedKey: (NSString*)aKey { NSDictionary *dict; NSException *exp; #ifdef WANT_DEPRECATED_KVC_COMPAT static IMP o = 0; /* Backward compatibility hack */ if (o == 0){ o = [NSObject instanceMethodForSelector: @selector(handleTakeValue:forUnboundKey:)]; } if ([self methodForSelector: @selector(handleTakeValue:forUnboundKey:)] != o){ [self handleTakeValue: anObject forUnboundKey: aKey]; return; } #endif dict = [NSDictionary dictionaryWithObjectsAndKeys: (anObject ? (id)anObject : (id)@"(nil)"), @"NSTargetObjectUserInfoKey", (aKey ? (id)aKey : (id)@"(nil)"), @"NSUnknownUserInfoKey", nil ]; exp = [NSException exceptionWithName: NSUndefinedKeyException reason: @"Unable to set value for undefined key" userInfo: dict]; [exp raise]; }