iOS随记

KVC原理分析

2020-03-10  本文已影响0人  脚踏实地的小C

一、KVC简介

  KVC(Key-Value Coding)键值编码,是利用NSKeyValueCoding 非正式协议实现的一种机制,对象采用这种机制来提供对其属性的间接访问。
  当对象采用该协议时,可以通过简洁统一的方法来访问其属性。简单来说,就是我们在开发中可以通过 key 名直接访问对象的属性,或者对属性进行赋值操作,而不需要调用明确的存取方法。这样就允许我们在 运行时动态 地访问和修改对象的属性,而不是在编译时决定。

二、KVC原理分析

1、KVC设值过程

当我们去调用 setValue:值 forKey:名字 设值方法时,底层的执行机制大致如下:

KVC设值过程.png

1、程序会优先去调用 set<Key>_set<Key>setIs<Key> 方法,如果存在这些命名规则的方法,会直接调用该方法进行赋值。调用优先顺序如上所写

2、如果没有找到 步骤1 的方法,程序会去判断 + (BOOL)accessInstanceVariablesDirectly 方法的返回值,如果该方法返回值为 NO (默认返回 YES,在我们重写该方法时有可能返回NO,一般不会返回NO),则会执行 setValue: forUndefinedKey: 方法报错。

3、如果 步骤2 返回 YES,程序会 \color{red}{依次} 去查找命名方式为 _<key>_<isKey><key><isKey>形式的实例变量,加入存在该形式的实例变量,则会直接将我们调用方法的值赋值给该实例变量。

4、如果 步骤3 没有查找到符合规则的实例变量,程序就会去执行 setValue: forUndefinedKey: 方法进行报错。

读万卷书,不如行万里路。下面我们来实例验证下:
@interface CHJManager : NSObject
{
    @public
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;
}
@end
@implementation CHJManager

+(BOOL)accessInstanceVariablesDirectly {
    return YES;
}
- (void)setName:(NSString *)value{
    NSLog(@"%s - %@",__func__,value);
}
- (void)_setName:(NSString *)value{
    NSLog(@"%s - %@",__func__,value);
}
- (void)setIsName:(NSString *)value{
    NSLog(@"%s - %@",__func__,value);
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"糊涂了吧,没有这个!");
}
@end
看到这,你们是不是会觉得为什么我们是用 成员变量,而不是用属性来做分析呢?

  其实是因为成员变量的 单一变量 原则,用属性的话,我们无法来区分。
执行代码:

    CHJManager *manager = [[CHJManager alloc] init];
    [manager setValue:@"帅帅金" forKey:@"name"];
    
    NSLog(@"%@ - %@ - %@ - %@",manager->_name,manager->_isName,manager->name,manager->isName);
    NSLog(@"%@ - %@ - %@",manager->_isName,manager->name,manager->isName);
    NSLog(@"%@ - %@",manager->name,manager->isName);
    NSLog(@"%@",manager->isName);

打印信息:

2020-03-09 16:09:06.631905+0800 KVC简介[33111:1035025] -[CHJManager setName:] - 帅帅金
2020-03-09 16:09:06.632019+0800 KVC简介[33111:1035025] (null) - (null) - (null) - (null)
2020-03-09 16:09:06.632110+0800 KVC简介[33111:1035025] (null) - (null) - (null)
2020-03-09 16:09:06.632196+0800 KVC简介[33111:1035025] (null) - (null)
2020-03-09 16:09:06.632343+0800 KVC简介[33111:1035025] (null)

  由上可知,程序执行了 setName 方法,验证了 步骤1。如果我们注释了 setName,它会执行 _setName方法,由此我们可知顺序依次为:setName_setNamesetIsName

  如果我们将 set方法都注释掉,然后再将 accessInstanceVariablesDirectly 返回值改为 NO 会发生什么呢?

2020-03-09 16:28:07.921634+0800 KVC简介[33560:1105250] 糊涂了吧,没有这个!
2020-03-09 16:28:07.921771+0800 KVC简介[33560:1105250] (null) - (null) - (null) - (null)
2020-03-09 16:28:07.921848+0800 KVC简介[33560:1105250] (null) - (null) - (null)
2020-03-09 16:28:07.921933+0800 KVC简介[33560:1105250] (null) - (null)
2020-03-09 16:28:07.922010+0800 KVC简介[33560:1105250] (null)

  和我们猜想的一样,走了 setValue: forUndefinedKey: 方法,由此 步骤2 得到了验证。

  接下来我们再将accessInstanceVariablesDirectly 返回值改为 YES

2020-03-09 16:29:49.127702+0800 KVC简介[33603:1110814] 帅帅金 - (null) - (null) - (null)
2020-03-09 16:29:49.127874+0800 KVC简介[33603:1110814] (null) - (null) - (null)
2020-03-09 16:29:49.127959+0800 KVC简介[33603:1110814] (null) - (null)
2020-03-09 16:29:49.128148+0800 KVC简介[33603:1110814] (null)

  perfect,可以看到我们的值被赋值给 _name ,而其他的三个实例变量为空。如果我们将成员变量一一注释,就可以得出查找顺序依次为 _<key>_is<Key><key>is<Key>,至此 步骤3 也得到了完美的验证。

2、KVC取值过程

1、首先按照 get<key><key>is<key>_<key> 的顺序 查找方法 ,如果找到方法,跳转到 步骤5 ,否则执行下一步。

2、如果没有找到上面的方法,KVC就会继续查找 countOf<Key>, objectIn<Key>AtIndex:(对应NSArray的方法), <key>AtIndexes: (对应NSArray 的 objectsAtIndexes: 方法)格式的方法,判断是否是属于 NSArray

3、查找 countOf<Key>enumeratorOf<Key>memberOf<Key>: 判断是否是属于 NSSet

4、如果上述方法都不存在,判断对象的类方法 accessInstanceVariablesDirectly 返回值,如果返回 YES则按顺序查找实例变量 _<key>_is<Key><key>is<Key>,如果查询到符合条件的实例变量,会直接取出实例变量的值,然后进行到 步骤5。反之,直接到 步骤6

5、 如果 步骤4 获取到的属性值是一个 对象指针,直接返回结果;
  如果该值是 NSNumber 支持的标量类型,将其存储为 NSNumber 类型的实例然后返回;
  如果该值 不是 NSNumber 支持的标量类型,将其转换为 NSValue 对象然后返回。

6、调用 valueForUndefinedKey: 方法进行报错。

实例验证:
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet   *set;

@property (nonatomic, strong) NSMutableArray        *namesArrM;
@property (nonatomic, strong) NSMutableSet          *namesSetM;
@property (nonatomic, strong) NSMutableOrderedSet   *orderedSetM;
#pragma mark - getter
-(NSString *)getName {
    return NSStringFromSelector(_cmd);
}
-(NSString *)name {
    return NSStringFromSelector(_cmd);
}
-(NSString *)isName {
    return NSStringFromSelector(_cmd);
}
-(NSString *)_name {
    return NSStringFromSelector(_cmd);
}
- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"没找到");
    return key;
}
    //执行代码:
    CHJManager *manager = [[CHJManager alloc] init];
    manager->_name = @"_name";
    manager->_isName = @"_isName";
    manager->name = @"name";
    manager->isName = @"isName";
    NSLog(@"取值:%@",[manager valueForKey:@"name"]);

打印信息:

2020-03-09 16:33:00.970505+0800 KVC简介[35291:1326801] 取值:getName

通过打印信息,我们可以看到,首先是按 步骤1 所说的先 查找方法

如果在 步骤1 中没找到方法呢?

- (NSUInteger)countOfPens {
    NSLog(@"- %s -", __func__);
    return [self.array count];
}
- (id)objectInPensAtIndex:(NSUInteger)index {
    NSLog(@"- %s -", __func__);
    return self.array[index];
}
  //验证代码
    manager.array = @[@"pen0", @"pen1", @"pen2", @"pen3", @"pen4"];
    NSArray *array = [manager valueForKey:@"pens"];
    NSLog(@"%@", [array objectAtIndex:1]);
    NSLog(@"数量 %ld", [array count]);
    NSLog(@"是否存在该值 %d", [array containsObject:@"pen2"]);

  这里我们对 countOf<key>objectIn<key>AtIndex:方法进行重写,所以后面我们通过[manager valueForKey:@"pens"] 就可以获取到数组 array 的值。

打印信息如下:

2020-03-10 10:20:12.953986+0800 KVC简介[5875:296832] - -[CHJManager objectInPensAtIndex:] -
2020-03-10 10:20:12.954107+0800 KVC简介[5875:296832] pen1
2020-03-10 10:20:12.954185+0800 KVC简介[5875:296832] - -[CHJManager countOfPens] -
2020-03-10 10:20:12.954267+0800 KVC简介[5875:296832] 数量 5
2020-03-10 10:20:12.954343+0800 KVC简介[5875:296832] - -[CHJManager countOfPens] -
2020-03-10 10:20:12.954417+0800 KVC简介[5875:296832] - -[CHJManager countOfPens] -
2020-03-10 10:20:12.954492+0800 KVC简介[5875:296832] - -[CHJManager objectInPensAtIndex:] -
2020-03-10 10:20:12.954574+0800 KVC简介[5875:296832] - -[CHJManager objectInPensAtIndex:] -
2020-03-10 10:20:12.954647+0800 KVC简介[5875:296832] - -[CHJManager objectInPensAtIndex:] -
2020-03-10 10:20:12.954721+0800 KVC简介[5875:296832] - -[CHJManager objectInPensAtIndex:] -
2020-03-10 10:20:12.954851+0800 KVC简介[5875:296832] - -[CHJManager objectInPensAtIndex:] -
2020-03-10 10:20:12.955077+0800 KVC简介[5875:296832] 是否存在该值 1

  看到这,步骤2 也得到了进一步的验证。步骤3 类似,所以这里不举例验证了。步骤4 又跟上面 设值步骤2 一样,所以我们直接跳到 步骤5 来。

    //指针类型
    [manager setValue:@"CHJ" forKey:@"name"];
    [manager setValue:@26 forKey:@"age"];
    [manager setValue:@"小C" forKey:@"myName"];
    NSLog(@"%@ - %@ - %@",[manager valueForKey:@"name"],[manager valueForKey:@"age"],[manager valueForKey:@"myName"]);

    //NSNumber类型
    [manager setValue:[NSNumber numberWithInt:26] forKey:@"age"];
    NSLog(@"age:%@",[manager valueForKey:@"age"]);

   //非NSNumber类型
    ThreeFloats floats = {1., 2., 3.};
    NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [manager setValue:value forKey:@"threeFloats"];
    NSValue *reslut = [manager valueForKey:@"threeFloats"];
    NSLog(@"%@",reslut);
    
    ThreeFloats th;
    [reslut getValue:&th] ;
    NSLog(@"%f - %f - %f",th.x,th.y,th.z);

打印信息:

2020-03-10 10:57:00.003734+0800 KVC简介[6694:411097] CHJ - 26 - 小C
2020-03-10 10:57:00.004511+0800 KVC简介[6694:411097] age:26
2020-03-10 10:59:04.540073+0800 KVC简介[6694:411097] {length = 12, bytes = 0x0000803f0000004000004040}
2020-03-10 10:59:04.540182+0800 KVC简介[6694:411097] 1.000000 - 2.000000 - 3.000000

  至此,步骤5 也得到了进一步的验证, 步骤6 比较常见,这里就不详细说明了。

3、KVC使用keyPath

  在开发过程中,一个类的成员变量有可能是自定义类或其他的复杂数据类型,你可以用KVC获取该类,然后再用KVC来获取这个自定义类的属性,但是这样比较繁琐,对此,KVC提供了一个解决方案,那就是键路径keyPath。顾名思义,就是按照路径寻找key。

- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通过KeyPath来设值

实例验证如下:

    Person *person = [[Person alloc] init];
    person.name = @"帅帅金";
    self.person = person;
    NSString *str1 = self.person.name;
    NSString *str2 = [self valueForKeyPath:@"person.name"];
    NSLog(@"str1 == %@ str2 == %@",str1,str2);
    
    [self setValue:@"飞机" forKeyPath:@"person.name"];
    str1 = self.person.name;
    str2 = [self valueForKeyPath:@"person.name"];
    NSLog(@"str1 == %@ str2 == %@",str1,str2);

打印信息:

2020-03-10 09:30:02.340834+0800 KVC[4772:180246] str1 == 帅帅金 str2 == 帅帅金
2020-03-10 09:30:02.340974+0800 KVC[4772:180246] str1 == 飞机 str2 == 飞机

  我们可以看到,这里的 name 是另外一个类的,当我们给这个自定义类的属性进行读取值的时候,我们就可以直接用 keyPath,是可以正常的输出。

如果我们不用 keyPath,只用key呢?

    Person *person = [[Person alloc] init];
    person.name = @"帅帅金";
    self.person = person;
    NSString *str1 = self.person.name;
    NSString *str2 = [self valueForKey:@"person.name"];
    NSLog(@"str1 == %@ str2 == %@",str1,str2);
    
    [self setValue:@"飞机" forKey:@"person.name"];
    str1 = self.person.name;
    str2 = [self valueForKey:@"person.name"];
    NSLog(@"str1 == %@ str2 == %@",str1,str2);

打印信息:

*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<ViewController 0x7ffda7f03f00> valueForUndefinedKey:]: this class is not key value coding-compliant for the key person.name.'

  直接崩溃了,我们可以看到,因为直接使用的是key,就会把 person.name 整个当成 key 去寻找,找不到,会调用 valueForUndefinedKey: 相关方法抛出异常。

小结: KVC对于 keyPath 的搜索机制第一步是分离key,用小数点来分割key,然后再像普通key一样按照先前介绍的顺序搜索下去。所以,当我们的属性或者实例变量是基本的系统类型就可以用 key 进行赋值和取值,但是属性或者实例变量也是另外一个类的时候,想要对该类的属性使用KVC进行赋值和取值时,用keyPath 更简便

4、KVC处理异常

KVC处理值为 nil 异常

  KVC中最常见的异常就是不小心使用了错误的 key,或者在设值中不小心传递了 nil 的值。不用方,KVC中有专门的方法来处理这些异常。
  通常情况下,KVC不允许你要在调用 setValue: forKey:时对非对象传递一个 nil 的值,因为值类型不能为 nil。如果你不小心传了,KVC会调用 setNilValueForKey: 这个方法,抛出异常,所以一般而言最好还是重写这个方法。

@interface Test: NSObject {
    NSUInteger age;
}
@end
@implementation Test
- (void)setNilValueForKey:(NSString *)key {
    NSLog(@"不能将%@设成nil", key);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //Test生成对象
        Test *test = [[Test alloc] init];
        //通过KVC设值test的age
        [test setValue:nil forKey:@"age"];
        //通过KVC取值age打印
        NSLog(@"test的年龄是%@", [test valueForKey:@"age"]);
    }
    return 0;
}

打印信息:

2020-03-10 12:24:30.302134+0800 KVCKVO[35470:6258307] 不能将age设成nil
2020-03-10 12:24:30.302738+0800 KVCKVO[35470:6258307] test的年龄是0

KVC处理值为 UndefinedKey 异常

  通常情况下,KVC不允许你要在调用setValue:属性值 forKey:(或者keyPath)时对不存在的key进行操作。 不然,会报错forUndefinedKey发生崩溃,重写forUndefinedKey方法避免崩溃。

@interface Test: NSObject {
}
@end
@implementation Test
- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"出现异常,该key不存在%@",key);
    return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"出现异常,该key不存在%@", key);
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        //Test生成对象
        Test *test = [[Test alloc] init];
        //通过KVC设值test的age
        [test setValue:@10 forKey:@"age"];
        //通过KVC取值age打印
        NSLog(@"test的年龄是%@", [test valueForKey:@"age"]);
    }
    return 0;
}

打印信息:

2020-03-10 12:30:18.564680+0800 KVCKVO[35487:6277523] 出现异常,该key不存在age
2020-03-10 12:30:18.565190+0800 KVCKVO[35487:6277523] 出现异常,该key不存在age
2020-03-10 12:30:18.565216+0800 KVCKVO[35487:6277523] test的年龄是(null)
上一篇 下一篇

猜你喜欢

热点阅读