KVC全称是Key Value Coding,定义在NSKeyValueCoding.h文件中,是一个非正式协议。KVC提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量。
在NSKeyValueCoding中提供了KVC通用的访问方法,分别是getter方法valueForKey:和setter方法setValue:forKey:,以及其衍生的keyPath方法,这两个方法各个类通用的。并且由KVC提供默认的实现。
GNUstep和Cocoa都是基于OpenStep演变而来,所以底层实现有很多相似之处,可以通过GNUstep源码来分析KVC的底层实现。
一、Set方法
- (void) setValue: (id)anObject forKey: (NSString*)aKey
{
unsigned size = [aKey length] * 8;
char key[size + 1];
....
[aKey getCString: key
maxLength: size + 1
encoding: NSUTF8StringEncoding];
size = strlen(key);
SetValueForKey(self, anObject, key, size);
}
- 将传入的key转成字符数组,然后调用
SetValueForKey()
,主要实现内容在SetValueForKey()
- 中间省略的部分为老版本的兼容设置
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];
char lo;
char hi;
strncpy(buf, "_set", 4);
strncpy(&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);
if (sel == 0 || [self respondsToSelector: sel] == NO)
{
name = buf; // _setKey:
sel = sel_getUid(name);
if (sel == 0 || [self respondsToSelector: sel] == NO)
{
sel = 0;
if ([[self class] accessInstanceVariablesDirectly] == YES)
{
buf[size + 4] = '\0';
buf[3] = '_';
buf[4] = lo;
name = &buf[3]; // _key
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);
}
此方法的实现很简单,主要是获取key相关的方法或者变量,然后调用GSObjCSetVal()
进行实际赋值操作
- 方法中利用传进来的字符数组,进行拼接,依次查询
setKey
--->_setKey
--->_key
--->_isKey
--->key
--->isKey
-
accessInstanceVariablesDirectly
通过此参数判断当关联方法找不到时,是否可以直接操作实例变量,默认实现为YES - 最后
GSObjCSetVal()
传入查询到的方法或者变量,进行实际赋值操作
void
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_ID:
case _C_CLASS:
{
id v = val;
if (sel == 0)
{
id *ptr = (id *)((char *)self + offset);
ASSIGN(*ptr, v);
}
else
{
void (*imp)(id, SEL, id) =
(void (*)(id, SEL, id))[self methodForSelector: sel];
(*imp)(self, sel, val);
}
}
break;
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;
case _C_UCHR:
case _C_UCHR:
case _C_SHT:
case _C_USHT:
case _C_INT:
...
default:
[self setValue: val forUndefinedKey:
[NSString stringWithUTF8String: key]];
}
}
}
此方法为KVC中实际赋值方法,主要做了一下几件事:
- 判断方法是否存在,如果存在,判断方法类型是否正确,set方法的参数应该有三个
self _cmd value
, 不正确就抛出异常, 如果方法正确,则通过getArgumentTypeAtIndex
获取到方法中value
参数的类型 - 此时如果方法存在,则获取到方法中参数的类型,如果方法不存在,则会使用实参传递进来的实例变量的类型
- 判断类型是否为空,如果为空,则代表没有key相关的信息,调用
setValue:forUndefinedKey:
方法,方法默认实现为抛出异常,子类实现此方法可避免崩溃 - 然后判断要赋值的
value
如果为nil/null
,并且参数类型不是指针类型和类,则调用setNilValueForKey:
,默认实现为抛出异常 - 此时已经获取了所有信息,判断参数类型,在根据获取的相关联信息,是方法还是实例变量进行不同操作,
如果是实例变量,则将参数转成相匹配类型,然后利用指针赋值
如果是方法,则根据methodForSelector:
获取相应类型的IMP
指针进行调用。 - 最后如果类型匹配不到,则调用
setValue:forUndefinedKey:
方法抛出异常
二、get方法
- (id) valueForKey: (NSString*)aKey
{
unsigned size = [aKey length] * 8;
char key[size + 1];
[aKey getCString: key
maxLength: size + 1
encoding: NSUTF8StringEncoding];
size = strlen(key);
return ValueForKey(self, key, size);
}
static id ValueForKey(NSObject *self, const char *key, unsigned size)
{
SEL sel = 0;
int off = 0;
const char *type = NULL;
if (size > 0)
{
const char *name;
char buf[size + 5];
char lo;
char hi;
strncpy(buf, "_get", 4);
strncpy(&buf[4], key, size);
buf[size + 4] = '\0';
lo = buf[4];
hi = islower(lo) ? toupper(lo) : lo;
buf[4] = hi;
name = &buf[1]; // getKey
sel = sel_getUid(name);
if (sel == 0 || [self respondsToSelector: sel] == NO)
{
buf[4] = lo;
name = &buf[4]; // key
sel = sel_getUid(name);
if (sel == 0 || [self respondsToSelector: sel] == NO)
{
buf[4] = hi;
buf[3] = 's';
buf[2] = 'i';
name = &buf[2]; // isKey
sel = sel_getUid(name);
if (sel == 0 || [self respondsToSelector: sel] == NO)
{
sel = 0;
}
}
}
if (sel == 0 && [[self class] accessInstanceVariablesDirectly] == YES)
{
buf[4] = hi;
name = buf; // _getKey
sel = sel_getUid(name);
if (sel == 0 || [self respondsToSelector: sel] == NO)
{
buf[4] = lo;
buf[3] = '_';
name = &buf[3]; // _key
sel = sel_getUid(name);
if (sel == 0 || [self respondsToSelector: sel] == NO)
{
sel = 0;
}
}
if (sel == 0)
{
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))
{
buf[4] = lo;
name = &buf[4]; // key
if (!GSObjCFindVariable(self, name, &type, &size, &off))
{
buf[4] = hi;
buf[3] = 's';
buf[2] = 'i';
name = &buf[2]; // isKey
GSObjCFindVariable(self, name, &type, &size, &off);
}
}
}
}
}
}
return GSObjCGetVal(self, key, sel, type, size, off);
}
id
GSObjCGetVal(NSObject *self, const char *key, SEL sel,
const char *type, unsigned size, int offset)
{
NSMethodSignature *sig = nil;
if (sel != 0)
{
sig = [self methodSignatureForSelector: sel];
if ([sig numberOfArguments] != 2)
{
[NSException raise: NSInvalidArgumentException
format: @"key-value get method has wrong number of args"];
}
type = [sig methodReturnType];
}
if (type == NULL)
{
return [self valueForUndefinedKey: [NSString stringWithUTF8String: key]];
}
else
{
id val = nil;
switch (*type)
{
case _C_ID:
case _C_CLASS:
{
id v;
if (sel == 0)
{
v = *(id *)((char *)self + offset);
}
else
{
id (*imp)(id, SEL) =
(id (*)(id, SEL))[self methodForSelector: sel];
v = (*imp)(self, sel);
}
val = v;
}
break;
case _C_UCHR:
case _C_UCHR:
case _C_SHT:
case _C_USHT:
case _C_INT:
....
default:
#ifdef __GNUSTEP_RUNTIME__
{
Class cls;
struct objc_slot *type_slot;
SEL typed;
struct objc_slot *slot;
cls = [self class];
type_slot = objc_get_slot(cls, @selector(retain));
typed = GSSelectorFromNameAndTypes(sel_getName(sel), NULL);
slot = objc_get_slot(cls, typed);
if (strcmp(slot->types, type_slot->types) == 0)
{
return slot->method(self, typed);
}
}
#endif
val = [self valueForUndefinedKey:
[NSString stringWithUTF8String: key]];
}
return val;
}
}
- get方法与set方法类似,都是根据key一次查询相关信息,查询顺序为
getKey
--->key
--->isKey
--->_getKey
--->_key(方法)
--->_key(实例变量)
--->_isKey
--->key
--->isKey
, 前5个查询为方法,后4个为实例变量。 - 同样根据
accessInstanceVariablesDirectly
判断是否直接操作实例变量,其中包含_getKey _key
方法 - 然后根据获取到的内容调用
GSObjCGetVal()
获取值 -
GSObjCGetVal()
和GSObjCSetVal()
逻辑相似,不过用来判断的是方法返回值的类型进行判断。同样,如果类型为空,则调用valueForUndefinedKey:
方法抛出异常。 - 然后根据类型、方法/实例变量进行取值,如果是方法,则获取
IMP
指针调用,如果只实例变量,则直接根据指针取值。 - 最后如果类型匹配不到,则调用
valueForUndefinedKey:
方法抛出异常。
三、KeyPath 相关
KVC中可以使用点语法,进行深层次的赋值、取值。
- (void) setValue: (id)anObject forKeyPath: (NSString*)aKey
{
NSRange r = [aKey rangeOfString: @"." options: NSLiteralSearch];
#ifdef WANT_DEPRECATED_KVC_COMPAT
IMP o = [self methodForSelector: @selector(takeValue:forKeyPath:)];
setupCompat();
if (o != takePath && o != takePathKVO)
{
(*o)(self, @selector(takeValue:forKeyPath:), anObject, aKey);
return;
}
#endif
if (r.length == 0)
{
[self setValue: anObject forKey: aKey];
}
else
{
NSString *key = [aKey substringToIndex: r.location];
NSString *path = [aKey substringFromIndex: NSMaxRange(r)];
[[self valueForKey: key] setValue: anObject forKeyPath: path];
}
}
- (id) valueForKeyPath: (NSString*)aKey
{
NSRange r = [aKey rangeOfString: @"." options: NSLiteralSearch];
if (r.length == 0)
{
return [self valueForKey: aKey];
}
else
{
NSString *key = [aKey substringToIndex: r.location];
NSString *path = [aKey substringFromIndex: NSMaxRange(r)];
return [[self valueForKey: key] valueForKeyPath: path];
}
}
两个方法类似,都是利用递归逐级调用 valueForKey:
以及 setValue:forKey:
最后
- KVC 就是利用字符串,来查询到和对象相关联的方法、实例变量,来达到间接访问属性方法或成员变量。
- 由于KVC在访问属性方法或成员变量前加了一层查询机制,所以效率没有直接访问属性方法或成员变量高。
- 由于KVC的使用是通过字符串, 所以极易出错,推荐借鉴 RAC中的
@keyPath
宏, RAC宏分析8:@keypath - 由上面源码得知,如果key相关联信息找不到,获取传入参数类型不匹配都会导致异常,所以使用时,最后要重写相应的方法
- 容器类包含了很多便捷的
keyPath
,
@count
@avg
@max
@min
@sum
@distinctUnionOfArrays
@distinctUnionOfObjects
@distinctUnionOfSets
@unionOfArrays
@unionOfObjects
@unionOfSets
以上keyPath
都是通过容器里重写valueForKeyPath:
实现,而且还可以通过数组,一次性返回内部对象的某个属性集合。
由于这些方法都是通过KVC实现,所以效率比较低,而且由于返回值为对象,所以还进行了不必要的封装
网友评论