iOS KVC底层源码

336 阅读7分钟

KVC官方文档

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 NSKeyValueCoding protocol provided by NSObject maps 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:, 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.

  • 基于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]];
        }
    }
}
  1. 从上面的源码可以看出SetValueForKey方法主要是获取key对应的sel(setKey或者_setKey),或者key对应的变量类型以及变量偏移量(变量名可能是_key, _isKey, key, isKey)

    获取key对应的变量类型以及变量偏移量前先通过accessInstanceVariablesDirectly 判断是否可以直接访问变量,如果可以,才按照变量名为(_key,_isKey,key,isKey) 的顺序查询变量类型以及变量偏移量

  2. 获取sel,变量的类型以及偏移量结束后,调用GSObjCSetVal进行最终的赋值操作。

    在GSObjCSetVal中判断如果sel不为空,则获取sel的方法签名,然后从方法签名中获取 变量类型,

  3. 如果在SetValueForKey中既获取不到sel,也获取不到变量类型,那么在GSObjCSetVal中 就会调用

    [self setValue: val forUndefinedKey:[NSString stringWithUTF8String: key]];

    后直接返回

  4. 如果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.

  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.

翻译过来就是:

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需要注意的地方

官方文档

  1. 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];
    }
    
  2. 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];
    }