KVC(Key-value coding):键值对编码,也就是咱们能够经过变量的名称来读取或者修改它的值,而不须要调用明确的存取方法。这样就能够在运行时动态地访问和修改对象的属性。而不是在编译时肯定。对于类里的私有属性,Objective-C是没法直接访问的,可是KVC是能够的。数组
做用:框架
kvc的经常使用方法有:ide
//经过Key来设值 - (void)setValue:(nullable id)value forKey:(NSString *)key; //经过KeyPath来设值 - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //直接经过Key来取值 - (nullable id)valueForKey:(NSString *)key; //经过KeyPath来取值 - (nullable id)valueForKeyPath:(NSString *)keyPath; //默认返回YES,表示是否容许直接访问变量 也就是若是没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索 + (BOOL)accessInstanceVariablesDirectly;
关于key和keyPath的区别:函数
1.在修改一个对象的属性的时候,forKey和forKeyPath,没什么区别:好比person有个name属性 name咱们经过kvc修改name属性的时候 这两个方法并无区别编码
[p setValue:@"jack" forKey:@"name"]; [p setValue:@"jack" forKeyPath:@"name"];
2.KeyPath方法中能够利用.运算符, 就能够一层一层往下查找对象的属性,而key方法不行:atom
如果层次结构深一点的。好比person 有dog对象;dog有bone属性时:spa
//这个是dog的属性: @class Bone; @interface Dog : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, strong) Bone *bone; @end //这个是bone的属性: @interface Bone : NSObject @property (nonatomic, strong) NSString *type; @end
咱们在经过kvc对bone属性进行赋值时:.net
//forKeyPath能使用点语法,深层次的去寻找咱们须要的属性 [p setValue:@"猪骨" forKeyPath:@"dog.bone.type"]; [p.dog setValue:@"猪骨" forKeyPath:@"bone.type"]; //这个方法也能够替换成forKey方法 [p.dog.bone setValue:@"猪骨" forKeyPath:@"type"];
此外还有一些不太经常使用的方法也能够了解下:设计
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError; //KVC提供属性值正确性�验证的API,它能够用来检查set的值是否正确、为不正确的值作一个替换值或者拒绝设置新值并返回错误缘由。 - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; //这是集合操做的API,里面还有一系列这样的API,若是属性是一个NSMutableArray,那么能够用这个方法来返回。 - (nullable id)valueForUndefinedKey:(NSString *)key; //若是Key不存在,且没有KVC没法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。 - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key; //和上一个方法同样,但这个方法是设值。 - (void)setNilValueForKey:(NSString *)key; //若是你在SetValue方法时面给Value传nil,则会调用这个方法 - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys; //输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
赋值顺序:3d
取值顺序:
1.KVC处理nil异常:
- (void)setNilValueForKey:(NSString *)key { NSLog(@"不能将%@设成nil", key); }
2.KVC处理UndefinedKey异常(也就是访问的属性名不存在):
一般状况下,KVC不容许你要在调用setValue:属性值 forKey:(或者keyPath)时对不存在的key进行操做。否则,会报错forUndefinedKey发生崩溃,
重写forUndefinedKey方法避免崩溃。
- (id)valueForUndefinedKey:(NSString *)key { NSLog(@"出现异常,该key不存在%@",key); return nil; } - (void)setValue:(id)value forUndefinedKey:(NSString *)key { NSLog(@"出现异常,该key不存在%@", key); }
以上重写的方法 都是写在被读写对象的类对象的m文件
字典转模型有不少方法,最直接的就是↓↓
User *user = [[User alloc] init]; user.name = dict[@"name"]; user.icon = dict[@"icon"]; ....
这种方法写了大量重复代码 不推荐;
也能够经过KVC↓↓↓:
[User setValuesForKeysWithDictionary:dict];
KVC底层的实现其实是将字典进行遍历,取出一个个Key 在模型中找同名的属性 探后将键值对中的Value赋值到属性中,这种方法的缺点就是字典中的Key必须在模型中都要找到对应的属性,不然会报setValue: forUndefinedKey的错误,引起程序crasha(这个能够经过重写setValue: forUndefinedKey方法解决)
/遍历 [dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { // 这行代码才是真正给模型的属性赋值 [s setValue:dict[@"source"] forKey:@"source"]; }];
此外,如今比较经常使用的是MJExtension框架(涉及到KVC+Runtime),它与KVO模型转字典流程不太同样:
kvo是遍历字典 根据key去模型中找属性;
而MJExtension是遍历模型中的属性,根据属性名去字典中取对应的Value进行赋值.
也就是:
1.拿到模型的属性名(注意属性名和成员变量名的区别),和对应的数据类型. 2.用该属性名做为键去字典中寻找对应的值. 3.拿到值后将值转换为属性对应的数据类型. 4.赋值.
//返回一个建立好的模型 + (instancetype)modelWithDict:(NSDictionary *)dict { //建立一个模型 id objc = [[self alloc] init]; int count = 0; /* 方法:获取成员变量列表 参数一:class获取哪一个类成员变量列表 参数二:count成员变量总数 */ // 成员变量数组 指向数组第0个元素 这里涉及到了Runtime Ivar *ivarList = class_copyIvarList(self, &count); // 遍历全部成员变量 for (int i = 0; i < count; i++) { // 获取成员变量 Ivar ivar = ivarList[i]; // 获取成员变量名称(将c转为oc) NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)]; // 成员变量名称转换key(将成员变量前边的"_"截取掉) NSString *key = [ivarName substringFromIndex:1]; // 从字典中取出对应value id value = dict[key]; // 给模型中属性赋值(底层会去找对应的属性和值) [objc setValue:value forKey:key]; } return objc; }
上面介绍的只是简单的一级转换,更多关于MJExtension的介绍能够看一下下面的相关资料↓↓↓
咱们在上篇文章中看到经过KVC修改属性值也是能够被KVO监听到的,咱们的第一个想法就是由于kvc在修改属性值的时候首先调用set方法,而遵照kvo的对象调用set方法其实是调用中间类对象重写的set方法,在中间类重写的set方法中进而对回调方法进行了调用。这个想法是正确的,正常状况下确实是由于调用了中间类的set方法从而被监听到。
可是,根据实验咱们也发现 若是经过kvc对某个对象的成员变量进行修改时 也能被kvo监听到,成员变量是没有set方法的。个人第一个想法是会不会是由于中间类的缘由。由于遵照kvo的对象由于isa是指向中间类的 而中间类会重写set方法 但仔细想一想也不对 由于中间类并非随便生成的 它是根据在最初的类对象基础上重写了某些方法而已 而原始的类对象自己就没有实现set方法 由于成员变量没有set方法 因此这个缘由是不成立的。那么缘由就是由于kvc的内部实现中手动调用了KVO(即手动调用了willChangeValueForKey:和didChangeValueForKey:方法)
咱们根据上面的赋值流程也知道 kvc是直接找到了变量直接进行赋值操做 那么其内部原理应该是这样的
在didChangeValueForKey方法内部会调用KVO的回调方法 从而实现监听
1 // 2 // ViewController.m 3 // kvcDemo 4 // 5 // Created by 人 on 2018/10/30. 6 // Copyright © 2018 洪. All rights reserved. 7 // 8 /* 9 KVC(Key-value coding)键值编码,就是指iOS的开发中,能够容许开发者经过Key名直接访问对象的属性,或者给对象的属性赋值。而不须要调用明确的存取方法。这样就能够在运行时动态地访问和修改对象的属性。而不是在编译时肯定,这也是iOS开发中的黑魔法之一。不少高级的iOS开发技巧都是基于KVC实现的。 10 11 KVC的定义都是对NSObject的扩展来实现的,Objective-C中有个显式的NSKeyValueCoding类别名,因此对于全部继承了NSObject的类型,都能使用KVC(一些纯Swift类和结构体是不支持KVC的,由于没有继承NSObject),下面是KVC最为重要的四个方法: 12 - (nullable id)valueForKey:(NSString *)key; //直接经过Key来取值 13 14 - (void)setValue:(nullable id)value forKey:(NSString *)key; //经过Key来设值 15 16 - (nullable id)valueForKeyPath:(NSString *)keyPath; //经过KeyPath来取值 17 18 - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //经过KeyPath来设值 19 20 其余的一些方法: 21 22 + (BOOL)accessInstanceVariablesDirectly; 23 //默认返回YES,表示若是没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索 24 25 - (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError; 26 //KVC提供属性值正确性�验证的API,它能够用来检查set的值是否正确、为不正确的值作一个替换值或者拒绝设置新值并返回错误缘由。 27 28 - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; 29 //这是集合操做的API,里面还有一系列这样的API,若是属性是一个NSMutableArray,那么能够用这个方法来返回。 30 31 - (nullable id)valueForUndefinedKey:(NSString *)key; 32 //若是Key不存在,且没有KVC没法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。 33 34 - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key; 35 //和上一个方法同样,但这个方法是设值。 36 37 - (void)setNilValueForKey:(NSString *)key; 38 //若是你在SetValue方法时面给Value传nil,则会调用这个方法 39 40 - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys; 41 //输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。 42 43 */ 44 /** 45 KVC设值 46 47 KVC要设值,那么就要对象中对应的key,KVC在内部是按什么样的顺序来寻找key的。当调用setValue:属性值 forKey:@”name“的代码时,底层的执行机制以下: 48 49 程序优先调用set<Key>:属性值方法,代码经过setter方法完成设置。注意,这里的<key>是指成员变量名,首字母大小写要符合KVC的命名规则,下同 50 51 若是没有找到setName:方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,若是你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:方法,不过通常开发者不会这么作。因此KVC机制会搜索该类里面有没有名为<key>的成员变量,不管该变量是在类接口处定义,仍是在类实现处定义,也不管用了什么样的访问修饰符,只在存在以<key>命名的变量,KVC均可以对该成员变量赋值。 52 53 若是该类即没有set<key>:方法,也没有_<key>成员变量,KVC机制会搜索_is<Key>的成员变量。 54 55 和上面同样,若是该类即没有set<Key>:方法,也没有_<key>和_is<Key>成员变量,KVC机制再会继续搜索<key>和is<Key>的成员变量。再给它们赋值。 56 57 若是上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。 58 59 简单来讲就是若是没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员并进行赋值操做。 60 61 若是开发者想让这个类禁用KVC里,那么重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO便可,这样的话若是KVC没有找到set<Key>:属性名时,会直接用setValue:forUndefinedKey:方法。 62 **/ 63 64 65 #import "ViewController.h" 66 #import "personClass.h" 67 @interface ViewController () 68 69 @end 70 71 @implementation ViewController 72 + (BOOL)accessInstanceVariablesDirectly { 73 return NO; 74 } 75 76 - (id)valueForUndefinedKey:(NSString *)key { 77 NSLog(@"出现异常,该key不存在%@",key); 78 return nil; 79 } 80 81 - (void)setValue:(id)value forUndefinedKey:(NSString *)key { 82 NSLog(@"出现异常,该key不存在%@", key); 83 } 84 85 - (void)viewDidLoad { 86 [super viewDidLoad]; 87 personClass *cla = [[personClass alloc]init]; 88 cla.name = @"小"; 89 90 // 对于类里的私有属性,Objective-C是没法直接访问的,可是KVC是能够的。 91 //这里的address是personClass的私有属性 因此没法经过点语法来访问 92 [cla setValue:@"上海市" forKey:@"address"]; 93 94 //经过KVC取值的话若是取到的值是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。 95 NSLog(@"%@是%@的人,年龄是%@",cla.name,[cla valueForKey:@"address"],[cla valueForKey:@"age"]); 96 //kvo是不容许传空值的 传空值的话会调用setNilValueForKey 97 [cla setValue:nil forKey:@"age"]; 98 99 //kvc赋值的时候 不能直接将一个数值经过KVC赋值的,咱们须要把数据转为NSNumber和NSValue类型传入 100 [cla setValue:[NSNumber numberWithInt:21] forKey:@"age"]; 101 102 103 /* 104 KVC同时还提供了很复杂的函数,主要有下面这些: 105 简单集合运算符共有@avg, @count , @max , @min ,@sum5种,分别是求平均数/求个数/求最大值/求最小值和求和 106 */ 107 personClass *cla1 = [[personClass alloc]init]; 108 [cla1 setValue:[NSNumber numberWithInt:21] forKey:@"age"]; 109 110 personClass *cla2 = [[personClass alloc]init]; 111 [cla2 setValue:[NSNumber numberWithInt:1] forKey:@"age"]; 112 113 personClass *cla3 = [[personClass alloc]init]; 114 [cla3 setValue:[NSNumber numberWithInt:21] forKey:@"age"]; 115 116 personClass *cla4 = [[personClass alloc]init]; 117 [cla4 setValue:[NSNumber numberWithInt:42] forKey:@"age"]; 118 119 NSArray *claAry = @[cla1,cla2,cla3,cla4]; 120 //KVC对于keyPath是搜索机制第一步就是分离key,用小数点.来分割key,而后再像普通key同样按照先前介绍的顺序搜索下去。 121 NSNumber* sum = [claAry valueForKeyPath:@"@sum.age"]; 122 NSLog(@"sum:%f",sum.floatValue); 123 NSNumber* avg = [claAry valueForKeyPath:@"@avg.age"]; 124 NSLog(@"avg:%f",avg.floatValue); 125 NSNumber* count = [claAry valueForKeyPath:@"@count"]; 126 NSLog(@"count:%f",count.floatValue); 127 NSNumber* min = [claAry valueForKeyPath:@"@min.age"]; 128 NSLog(@"min:%f",min.floatValue); 129 NSNumber* max = [claAry valueForKeyPath:@"@max.age"]; 130 NSLog(@"max:%f",max.floatValue); 131 132 /* 133 比集合运算符稍微复杂,能以数组的方式返回指定的内容,一共有两种: 134 135 @distinctUnionOfObjects 136 @unionOfObjects 137 它们的返回值都是NSArray,区别是前者返回的元素都是惟一的,是去重之后的结果;后者返回的元素是全集。 138 */ 139 140 //某一个数据中某个属性的值集合去重后结果 141 NSArray* arrDistinct = [claAry valueForKeyPath:@"@distinctUnionOfObjects.age"]; 142 for (NSNumber *age in arrDistinct) { 143 NSLog(@"%f",age.floatValue); 144 } 145 146 ////某一个数据中某个属性的值集合结果 147 NSArray* arrUnion = [claAry valueForKeyPath:@"@unionOfObjects.age"]; 148 for (NSNumber *age in arrUnion) { 149 NSLog(@"%f",age.floatValue); 150 } 151 152 //经过kvc进行字典转模型 153 NSDictionary *dic = @{@"name":@"ming",@"age":@14,@"address":@"北京市",@"ds":@"dsad"}; 154 /* 155 KVC里面还有两个关于NSDictionary的方法: 156 157 - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys; 158 - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues; 159 160 setValuesForKeysWithDictionary是用来修改Model中对应key的属性。下面直接用代码会更直观一点:这个用来字典转模型 161 */ 162 personClass *p = [[personClass alloc]init]; 163 //字典转模型 164 [p setValuesForKeysWithDictionary:dic]; 165 /* 166 KVC的实现模式是取出字典中的键值,去模型中找与之对应的属性,那么反之考虑,咱们能不能抓取模型中的属性对象,去字典中找对应的键值呢?因此这就要考虑用到运行时机制runtime了。咱们先获取到模型对象的属性名,将他们加入到一个数组当中,而后遍历数组,在遍历过程当中给属性对象赋值。这也是KVC和runtime用于实现字典转模型的区别之一。 167 runtime是遍历模型的属性 若是这个属性能够从字典中找到对应的属性值就赋值 找不到就判断下一个模型属性 168 kvc是遍历字典的键值对 若是这个键值对的key是模型中的一个属性 那么就对模型进行赋值操做 若是这个key不是模型属性那么则判断下一个键值对的key 169 */ 170 NSLog(@"%@岁的%@住在%@",[p valueForKey:@"age"],p.name,[p valueForKey:@"address"]); 171 172 173 personClass *erp = [[personClass alloc]init]; 174 erp.name = @"大名"; 175 [erp setValue:@"提阿尼" forKey:@"address"]; 176 [erp setValue:[NSNumber numberWithInt:12] forKey:@"age"]; 177 178 NSArray *mesAry = @[@"age",@"name"]; 179 // dictionaryWithValuesForKeys:是指输入一组key,返回这组key对应的属性,再组成一个字典。 180 NSDictionary *dicMes = [erp dictionaryWithValuesForKeys:mesAry]; 181 NSLog(@"%@",dicMes); 182 /** 183 KVC的设计原理: 184 185 [item setValue:@"value" forKey:@"property"]: 186 187 1.首先去模型中查找有没有setProperty,找到,直接调用赋值 [self setProperty:@"value"] 188 189 2.去模型中查找有没有property属性,有,直接访问属性赋值 property = value 190 191 3.去模型中查找有没有_property属性,有,直接访问属性赋值 _property = value 192 193 4.找不到,就会直接报错 setValue:forUndefinedKey:报找不到的错误 194 **/ 195 196 /*不少UI控件都由不少内部UI控件组合而成的,可是Apple度没有提供这访问这些控件的API,这样咱们就没法正常地访问和修改这些控件的样式。 197 而KVC在大多数状况可下能够解决这个问题。最经常使用的就是个性化UITextField中的placeHolderText了。 198 */ 199 200 201 }