KVC原理

1,214 阅读7分钟

一、概述

探究苹果某个api时候我们首先先从苹果的官方文档中查看该api的描述,这种方法叫做文档学习法。

这篇我看探究下KVC原理,首先我们进到官网上看看官方文档对KVC的描述KVC官方文档,根据文档描述

  • KVC是根据非正式协议间接访问对象成员变量的一种方法

并且文档中有相应api的介绍以及例子

二、用法

下面我们来看看KVC的一些用法:

1、基本用法

直接对属性或者成员变量进行取值和赋值

    LGPerson *person = [[LGPerson alloc] init];
    [person setValue:@"KC" forKey:@"name"];
    [person setValue:@19 forKey:@"age"];
    [person setValue:@"酷C" forKey:@"myName"];
    NSLog(@"%@ - %@ - %@",[person valueForKey:@"name"],[person valueForKey:@"age"],[person valueForKey:@"myName"]);

2、操作集合类型

针对集合属性,可以直接通过mutableArrayValueForKey对对象的集合属性进行操作更改

    person.array = @[@"1",@"2",@"3"];
    // KVC 的方式
    NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
    ma[0] = @"100";
    NSLog(@"%@",[person valueForKey:@"array"]);

3、访问非对象属性

针对结构体,KVC也可以直接操作,但是操作时候需要将结构体转成NSValue类型,

typedef struct {
    float x, y, z;
} ThreeFloats;

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

4、层层访问

假如对象的属性也是对象,那么KVC可以通过keyPath来操作对象属性的属性

    LGStudent *student = [[LGStudent alloc] init];
    student.subject    = @"iOS";
    person.student     = student;
    [person setValue:@"大师班" forKeyPath:@"student.subject"];
    NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);

5、集合操作符

(1)字典操作

假如字典的key和一个对象的属性都一样,那么可以通过setValuesForKeysWithDictionary直接将字典的value赋值给对象相应的属性,同样,也可以通过dictionaryWithValuesForKeys将对象转换成字典

- (void)dictionaryTest{
    
    NSDictionary* dict = @{
                           @"name":@"Cooci",
                           @"nick":@"KC",
                           @"subject":@"iOS",
                           @"age":@18,
                           @"length":@180
                           };
    LGStudent *p = [[LGStudent alloc] init];
    // 字典转模型
    [p setValuesForKeysWithDictionary:dict];
    NSLog(@"%@:%@",p,p.name);
    // 键数组转模型到字典
    NSArray *array = @[@"name",@"age"];
    NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
    NSLog(@"%@",dic);
}

(2)操作数组元素的信息(KVC消息传递)

通过api可以拿到数组元素的长度,也可以对数组元素进行操作得到新的数组

    NSArray *array = @[@"Hank",@"Cooci",@"Kody",@"CC"];
    //得到数组中所有元素的长度
    NSArray *lenStr= [array valueForKeyPath:@"length"];
    NSLog(@"%@",lenStr);
    //将数组中所有值全变成小写
    NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
    NSLog(@"%@",lowStr);
    //将数组中所有值全变成大写
    NSArray *uppercaseStr= [array valueForKeyPath:@"uppercaseString"];
    NSLog(@"%@",uppercaseStr);

(3)、聚合操作符

@avg:取平均值 @count:取个数 @max:取最大值 @min:取最小值 @sum:求和

- (void)aggregationOperator{
    NSMutableArray *personArray = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGStudent *p = [LGStudent new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [p setValuesForKeysWithDictionary:dict];
        [personArray addObject:p];
    }
    NSLog(@"%@", [personArray valueForKey:@"length"]);
    
    /// 平均身高
    float avg = [[personArray valueForKeyPath:@"@avg.length"] floatValue];
    NSLog(@"%f", avg);
    
    int count = [[personArray valueForKeyPath:@"@count.length"] intValue];
    NSLog(@"%d", count);
    
    int sum = [[personArray valueForKeyPath:@"@sum.length"] intValue];
    NSLog(@"%d", sum);
    
    int max = [[personArray valueForKeyPath:@"@max.length"] intValue];
    NSLog(@"%d", max);
    
    int min = [[personArray valueForKeyPath:@"@min.length"] intValue];
    NSLog(@"%d", min);
}

(4)、数组操作符

可以做去重等操作

- (void)arrayOperator{
    NSMutableArray *personArray = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGStudent *p = [LGStudent new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [p setValuesForKeysWithDictionary:dict];
        [personArray addObject:p];
    }
    NSLog(@"%@", [personArray valueForKey:@"length"]);
    // 返回操作对象指定属性的集合
    NSArray* arr1 = [personArray valueForKeyPath:@"@unionOfObjects.nick"];
    NSLog(@"arr1 = %@", arr1);
    // 返回操作对象指定属性的集合 -- 去重
    NSArray* arr2 = [personArray valueForKeyPath:@"@distinctUnionOfObjects.length"];
    NSLog(@"arr2 = %@", arr2);
    
}

5、嵌套操作

@distinctUnionOfArrays:去重取值 @unionOfArrays:取值

- (void)arrayNesting{
    
    NSMutableArray *personArray1 = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGStudent *student = [LGStudent new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [student setValuesForKeysWithDictionary:dict];
        [personArray1 addObject:student];
    }
    
    NSMutableArray *personArray2 = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGPerson *person = [LGPerson new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [person setValuesForKeysWithDictionary:dict];
        [personArray2 addObject:person];
    }
    
    // 嵌套数组
    NSArray* nestArr = @[personArray1, personArray2];
    
    NSArray* arr = [nestArr valueForKeyPath:@"@distinctUnionOfArrays.length"];
    NSLog(@"arr = %@", arr);
    
    NSArray* arr1 = [nestArr valueForKeyPath:@"@unionOfArrays.length"];
    NSLog(@"arr1 = %@", arr1);
}

(6)、嵌套操作二

- (void)setNesting{
    
    NSMutableSet *personSet1 = [NSMutableSet set];
    for (int i = 0; i < 6; i++) {
        LGStudent *person = [LGStudent new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [person setValuesForKeysWithDictionary:dict];
        [personSet1 addObject:person];
    }
    NSLog(@"personSet1 = %@", [personSet1 valueForKey:@"length"]);
    
    NSMutableSet *personSet2 = [NSMutableSet set];
    for (int i = 0; i < 6; i++) {
        LGPerson *person = [LGPerson new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [person setValuesForKeysWithDictionary:dict];
        [personSet2 addObject:person];
    }
    NSLog(@"personSet2 = %@", [personSet2 valueForKey:@"length"]);

    // 嵌套set
    NSSet* nestSet = [NSSet setWithObjects:personSet1, personSet2, nil];
    // 交集
    NSArray* arr1 = [nestSet valueForKeyPath:@"@distinctUnionOfSets.length"];
    NSLog(@"arr1 = %@", arr1);
}

三、KVC取值和赋值的过程

1、赋值

a、官方解释

我们在官方文档上对kvc的赋值方法有下面解释

  • 1、在调用setValue:forKey时候,系统会先去找set:和_set执行
  • 2、如果没找到,那么再判断accessInstanceVariablesDirectly是不是返回YES,如果是,那么就找_, _is, , or is这样的成员变量进行赋值。
  • 3、如果都没找到,那么就抛出setValue:forUndefinedKey:的异常

2、取值

我们看看官方文档对KVC的取值方法的描述

  • 1、先找 get, , is, or _执行
  • 2、如果属性是NSArray类型,并且对数组属性执行的是objectAtIndex方法,那么就就找objectInAtIndex:方法
  • 3、如果没有,那么再判断accessInstanceVariablesDirectly是不是返回YES,如果是YES,则去找_, _is, , or is成员变量
  • 4、如果成员变量没有,则抛出valueForUndefinedKey:.错误

3、验证

首先我们创建一个类,因为属性会生成set和get方法,会干扰我们验证,所以我们创建几个成员变量

@interface LGPerson : NSObject{
    @public
     NSString *_name;
     NSString *_isName;
     NSString *name;
     NSString *isName;
}
@property (nonatomic, strong) NSArray *arr;

@end

@implementation LGPerson

#pragma mark - 关闭或开启实例变量赋值
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

//MARK: - setKey. 的流程分析
//- (void)setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

//- (void)_setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

//- (void)setIsName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}


//MARK: - valueForKey 流程分析 - get<Key>, <key>, is<Key>, or _<key>,

//- (NSString *)getName{
//    return NSStringFromSelector(_cmd);
//}

//- (NSString *)name{
//    return NSStringFromSelector(_cmd);
//}

//- (NSString *)isName{
//    return NSStringFromSelector(_cmd);
//}

//- (NSString *)_name{
//    return NSStringFromSelector(_cmd);
//}
@end

然后有下面代码测试:

LGPerson *person = [[LGPerson alloc] init];
    
    // 1: KVC - 设置值的过程
     [person setValue:@"LG_Cooci" forKey:@"name"];
NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
    NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
    NSLog(@"%@-%@",person->name,person->isName);
    NSLog(@"%@",person->isName);

    // 2: KVC - 取值的过程
     person->_name = @"_name";
     person->_isName = @"_isName";
     person->name = @"name";
     person->isName = @"isName";

     NSLog(@"取值:%@",[person valueForKey:@"name"]);

四、KVC的异常小技巧

1、KVC能自动转换类型

KVC能自动把字符串类型的value转换成对应的int、bool等类型

    LGPerson *person = [[LGPerson alloc] init];
    [person setValue:@18 forKey:@"age"];
    // 上面那个表达 大家应该都会! 但是下面这样操作也可以
    [person setValue:@"20" forKey:@"age"]; // int - string
    NSLog(@"%@-%@",[person valueForKey:@"age"],[[person valueForKey:@"age"] class]);//__NSCFNumber
    [person setValue:@"20" forKey:@"sex"];
    NSLog(@"%@-%@",[person valueForKey:@"sex"],[[person valueForKey:@"sex"] class]);//__NSCFNumber

其中LGPerson的age是int类型,set是bool类型 打印的内容:

2020-02-18 19:19:38.651192+0800 004-KVC异常小技巧[96796:3007843] 20-__NSCFNumber
2020-02-18 19:19:38.651351+0800 004-KVC异常小技巧[96796:3007843] 1-__NSCFBoolean

同样,对结构体也有用

2、设置空值

当对NSNumber和NSValue类型的成员变量赋值为nil时候会崩溃,也就是会对int、BOOL、float等类型的成员变量

    [person setValue:nil forKey:@"age"]; // subject不会走 - 官方注释里面说只对 NSNumber - NSValue

解决办法是,写个分类,重写setNilValueForKey方法即可

- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"你傻不傻: 设置 %@ 是空值",key);
}

对了非NSNumber为啥不会崩溃呢,因为对于非NSNumber,nil本身就是其中的一个值

3、赋值时候找不到的 key

当对不存在的key进行赋值时候也会崩溃,解决办法时候就是在分类中重写下面方法即可:

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"你瞎啊: %@ 没有这个key",key);
}

4、取值时候找不到key

对不存在的key进行取值时候也会崩溃,解决办法就是在分类中重写下面方法:

- (id)valueForUndefinedKey:(NSString *)key{
    NSLog(@"你瞎啊: %@ 没有这个key - 给你一个其他的吧,别奔溃了!",key);
    return @"Master 牛逼";
}

5、键值验证

当我们不知道对象是否有某个属性时候我们可以用validateValue方法先验证一下,然后再对属性进行操作

    NSError *error;
    NSString *name = @"LG_Cooci";
    if (![person validateValue:&name forKey:@"names" error:&error]) {
        NSLog(@"%@",error);
    }else{
        NSLog(@"%@",[person valueForKey:@"name"]);
    }

或者在分类中重写该方法,同时也可以通过该方法进行赋值转发,将value给其他属性

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing  _Nullable *)outError{
    if([inKey isEqualToString:@"names"]){
        [self setValue:[NSString stringWithFormat:@"里面修改一下: %@",*ioValue] forKey:@"name"];
        return YES;
    }
    *outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ 不是 %@ 的属性",inKey,self] code:10088 userInfo:nil];
    return NO;
}