iOS底层学习——KVC上篇

2021-08-25 11:13:00 浏览数 (1)

1.KVC协议定义

键值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该机制来提供对其属性的间接访问。当对象符合键值编码时,其属性可通过字符串参数通过简洁、统一的消息传递接口进行寻址。这种间接访问机制补充了实例变量及其相关访问器方法提供的直接访问。

KVC在Objective-C中的定义:KVC的定义都是对NSObject的,扩展来实现的(Objective-C中有个显式的 NSKeyValueCoding类别名-分类)。 查看setValueForKey方法,发现其在Foundation里面,而Foundation框架是不开源的,只能在苹果官方文档查找。见下图:

2.KVC提供的API方法

我们可以学习解读苹果的官方文档,对KVC有更深的理解。

Key-Value Coding Programming Guide

苹果对一些容器类比如NSArray或者NSSet等,KVC有着特殊的实现。

常用方法:对于所有继承了NSObject的类型,也就是几乎所有的Objective-C对象都能使用KVC,下面是KVC最为重要的四个方法:

代码语言:javascript复制
  - (nullable id)valueForKey:(NSString *)key;                          // 直接通过Key来取值
   - (void)setValue:(nullable id)value forKey:(NSString *)key;          // 通过Key来设值
   - (nullable id)valueForKeyPath:(NSString *)keyPath;                  // 通过KeyPath来取值
   - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  // 通过KeyPath来设值

滑动显示更多

特殊方法

当然NSKeyValueCoding类别中还有其他的一些方法,这些方法在碰到特殊情况或者有特殊需求还是会用到的。

代码语言:javascript复制
// 默认返回YES,表示如果没有找到Set方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
  (BOOL)accessInstanceVariablesDirectly;
// KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
// 和上一个方法一样,但这个方法是设值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// 如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;
// 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

滑动显示更多

结构体处理

KVC在进行结构体处理时,需要用到NSValue,设值时,将结构体封装成NSValue,进行键值设值;取值同样返回NSValue,然后按照结构体格式进行解析,见下面代码:

代码语言:javascript复制
    // 结构体
    ThreeFloats floats = {1.,2.,3.};
    // 封装成NSValue
    NSValue *value     = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    // 设值
    [person setValue:value forKey:@"threeFloats"];

    // 取值
    NSValue *value1    = [person valueForKey:@"threeFloats"];
    // 结构体解析
    ThreeFloats th;
    [value1 getValue:&th];
    NSLog(@"%f-%f-%f",th.x,th.y,th.z);

滑动显示更多

字典处理(模型转换)

字典可以实现与模型进行装换,也可以通过键值数组从模型中获取字典数据。见下面代码:

代码语言:javascript复制
- (void)dictionaryTest{
    // 字典
    NSDictionary* dict = @{
                           @"name":@"Cooci",
                           @"nick":@"KC",
                           @"subject":@"iOS",
                           @"age":@18,
                           @"length":@180
                           };
    // 模型
    LGStudent *p = [[LGStudent alloc] init];
    // 字典转模型
    [p setValuesForKeysWithDictionary:dict];

    // 键值数组
    NSArray *array = @[@"name",@"age"];
    // 从模型中获取响应的字典数据
    NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
    NSLog(@"%@",dic);
}

滑动显示更多

3.KVC设值取值顺序

KVC是怎么使用的,我们都很清楚,那么KVC在内部是按什么样的顺序来寻找key的呢?这是我们要探索的重点。

1.设值

当调用setValue:forKey:代码时,底层的执行机制是怎样的呢?在官方文档中有相关的说明,见下图:

翻译过来的意思是:

setValue:forKey: 的默认实现,给定key和value参数作为输入,尝试将名为key的属性设置为value,在接收调用的对象内部,使用以下过程:按顺序查找名为set<Key>: 或 _set<Key> 的第一个访问器。 如果找到,则使用输入值(或根据需要展开的值)调用它并完成。

如果未找到简单访问器,并且类方法 accessInstanceVariablesDirectly返回 YES,则按顺序查找名称类似于 _<key>、_is<Key>、<key> 或 is<Key> 的实例变量。

如果找到,直接使用输入值(或解包值)设置变量并完成。在未找到访问器或实例变量时,调用 setValue:forUndefinedKey:。默认情况下,这会引发异常,但 NSObject 的子类可能会提供特定于键的行为。

根据上的官方内容,可以得出如下实现机制:

按顺序查找名为set<Key>,_set<Key> 或者setIs<Key>的setter访问器顺序查找,如果找到就调用它。

只要实现任意一个,那么就会将调用这个方法,将属性的值设为传进来的值,如果没有找到这些setter方法。

KVC机制会检查 (BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,如果重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:方法。

如果返回YES,KVC机制会优先搜索该类里面有没有名为_<Key>的成员变量,无论该变量是在类接口处定义,还是在类实现处定义。

也无论用了什么样的访问修饰符,只在存在以_<Key>命名的变量,KVC都可以对该成员变量赋值。

KVC机制再会继续搜索_is<Key>、<key>和is<key>的成员变量,再给它们赋值如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。

可通过案例进行验证,这里不再展示。

代码语言:javascript复制
accessInstanceV [person setValue:@"newName" forKey:@"name"];

可以得出以下结论:优先通过setter方法,进行属性设置,调用顺序是:
setName

_setName

setIsName

如果以上方法均未找到,并且accessInstanceVariablesDirectly返回YES,

则通过成员变量进行设置,顺序是:

_name

_isName

name

滑动显示更多

isNameariablesDirectly说明:重写 (BOOL)accessInstanceVariablesDirectly方法让其返回NO,这样的话,如果KVC没有找到set<Key>、_set<Key>、setIs<Key>相关方法时。

会直接用setValue:forUndefinedKey方法。我们用代码来测试一下上面的KVC机制:

代码语言:javascript复制
@interface LGPerson : NSObject
{
    @public
        NSString *_isName;
        NSString *name;
        NSString *isName;
        NSString *_name;
}
@end
@implementation LGPerson

 (BOOL)accessInstanceVariablesDirectly{
    return NO;
}

-(id)valueForUndefinedKey:(NSString *)key{
    NSLog(@"出现异常,该key不存在%@",key);
    return nil;
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
     NSLog(@"出现异常,该key不存在%@",key);
}

// 设置方法全部注释掉
// -(void)setName:(NSString*)name{
//     toSetName = name;
// }
// - (void)_setName:(NSString *)name{
//     NSLog(@"%s - %@",__func__,name);
// }
// - (void)setIsName:(NSString *)name{
//     NSLog(@"%s - %@",__func__,name);
// }


@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGPerson* person = [LGPerson new];
        [person setValue:@"NewName" forKey:@"name"];
        NSString* name = [person valueForKey:@"name"];
        NSLog(@"value for key : %@",name);

        NSLog(@"取值_name:%@",person->_name);
        NSLog(@"取值_isName:%@",person->_isName);
        NSLog(@"取值name:%@",person->name);
        NSLog(@"取值isName:%@",person->isName);
    }
    return 0;
}

滑动显示更多

这说明了重写 (BOOL)accessInstanceVariablesDirectly方法让其返回NO后,KVC找不到set<Key>等方法后,不再去找<Key>系列成员变量。而是直接调用setValue:forUndefinedKey:方法,所以开发者如果不想让自己的类实现KVC,就可以这么做。

KVC设值流程图:

2.取值

当调用valueForKey:的代码时,底层的执行机制又是怎样的呢?在官方文档中有相关的说明,见下图:

根据上的官方内容,翻译之后可以得出如下实现机制:

首先按get<Key>,<Key>,is<Key>,_<Key>的顺序方法查找getter方法,找到的话会直接调用,如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。

如果上面的getter没有找到,KVC则会查countOf<Key>objectIn<Key>AtIndex或<Key>AtIndexes格式的方法。

如果countOf<Key>方法和另外两个方法中的一个被找到。那么就会返回一个可以响应NSArray。

所有方法的代理集合(NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法。

就会以countOf<Key>,objectIn<Key>AtIndex或At<Key>Indexes

这几个方法组合的形式调用。还有一个可选的get<Key>:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。

如果上面的方法没有找到那么会同时查找countOf<Key>enumeratorOf<Key>,memberOf<Key>格式的方法。

如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样给这个代理集合发NSSet的消息。 就以countOf<Key>enumeratorOf<Key>memberOf<Key>组合的形式调用。

问实例变量破坏了封装性,使代码更脆弱。

代码语言:javascript复制
  如果重写了类方法
  (BOOL)accessInstanceVariablesDirectly返回NO的话。
那么会直接调用valueForUndefinedKey:
   还没有找到的话,调用
valueForUndefinedKey:以[person valueForKey:@"name"];
为例getter方法的调用顺序是:
getName

name

isName

_name

如果以上方法没有找到,accessInstanceVariablesDirectly返回YES,则直接返回成员变量,获取顺序依然是:

_name

_isName

name

isName

可通过案例进行验证,这里不再展示。

滑动显示更多

KVC取值流程图:

代码语言:javascript复制
    @interface LGPerson : NSObject
    {
        @public
            NSString *_isName;
            NSString *name;
            NSString *isName;
            NSString *_name;
    }
    @end
    @implementation LGPerson

     (BOOL)accessInstanceVariablesDirectly{
        return NO;
    }

    -(id)valueForUndefinedKey:(NSString *)key{
        NSLog(@"出现异常,该key不存在%@",key);
        return nil;
    }
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
         NSLog(@"出现异常,该key不存在%@",key);
    }

    // 设置方法全部注释掉
    // -(void)setName:(NSString*)name{
    //     toSetName = name;
    // }
    // - (void)_setName:(NSString *)name{
    //     NSLog(@"%s - %@",__func__,name);
    // }
    // - (void)setIsName:(NSString *)name{
    //     NSLog(@"%s - %@",__func__,name);
    // }

    // 取值方法
    //- (NSString *)getName{
    //    return NSStringFromSelector(_cmd);
    //}
    //- (NSString *)name{
    //    return NSStringFromSelector(_cmd);
    //}
    //- (NSString *)isName{
    //    return NSStringFromSelector(_cmd);
    //}
    //- (NSString *)_name{
    //    return NSStringFromSelector(_cmd);
    //}
    @end

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            LGPerson* person = [LGPerson new];
            [person setValue:@"NewName" forKey:@"name"];
            NSString* name = [person valueForKey:@"name"];
            NSLog(@"value for key : %@",name);

            NSLog(@"取值_name:%@",person->_name);
            NSLog(@"取值_isName:%@",person->_isName);
            NSLog(@"取值name:%@",person->name);
            NSLog(@"取值isName:%@",person->isName);
        }
        return 0;
    }

0 人点赞