KVC

摘要:
对于类里的私有属性,Objective-C是无法直接访问的,但是KVC是可以的。-valueForUndefinedKey:key;//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。如果你不小心传了,KVC会调用setNilValueForKey:方法。

1、概念:

KVC(Key-value coding):键值对编码,也就是我们可以通过变量的名称来读取或者修改它的值,而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定。对于类里的私有属性,Objective-C是无法直接访问的,但是KVC是可以的

作用:

  • 取值和赋值(开发中基本不用)
  • 获取对象私有变量的值.(经常使用,例如UIPageContorl分页, 设置圆点为图片)
  • 改变对象私有变量的值(经常使用)
  • 简单的字典转模型(偶尔使用)
  • 模型转字典
  • 批量取值

kvc的常用方法有:

//通过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方法不行:

若是层次结构深一点的。比如person 有dog对象;dog有bone属性时:

//这个是dog的属性:
@classBone;
@interfaceDog : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) Bone *bone;
@end
//这个是bone的属性:
@interfaceBone : NSObject
@property (nonatomic, strong) NSString *type;
@end

我们在通过kvc对bone属性进行赋值时:

//forKeyPath能使用点语法,深层次的去寻找我们需要的属性
[p setValue:@"猪骨" forKeyPath:@"dog.bone.type"];
[p.dog setValue:@"猪骨" forKeyPath:@"bone.type"];
//这个方法也可以替换成forKey方法
    [p.dog.bone setValue:@"猪骨" forKeyPath:@"type"];
关于KeyPath的更多功能(比如对数组、字典或者NSSet的操作)

此外还有一些不太常用的方法也可以了解下:

- (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转到字典。

2、读写值顺序:

赋值顺序:

KVC第1张

取值顺序:

KVC第2张

3、常见异常

1.KVC处理nil异常:

通常情况下,KVC不允许你要在调用setValue:属性值 forKey:(或者keyPath)时对非对象传递一个nil的值。很简单,因为值类型是不能为nil的。如果你不小心传了,KVC会调用setNilValueForKey:方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。
- (void)setNilValueForKey:(NSString *)key {
    NSLog(@"不能将%@设成nil", key);
}

2.KVC处理UndefinedKey异常(也就是访问的属性名不存在):

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

重写forUndefinedKey方法避免崩溃。

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

以上重写的方法 都是写在被读写对象的类对象的m文件

KVC第3张

4、KVC+Runtime 字典转模型

字典转模型有很多方法,最直接的就是↓↓

 User *user =[[User alloc] init];
 user.name = dict[@"name"];
 user.icon = dict[@"icon"];
 ....

这种方法写了大量重复代码 不推荐;

也可以通过KVC↓↓↓:

[UsersetValuesForKeysWithDictionary: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];
    }
    returnobjc;
}

上面介绍的只是简单的一级转换,更多关于MJExtension的介绍可以看一下下面的相关资料↓↓↓

跟着MJExtension实现简单的字典转模型框架
[iOS]MJExtension 源码阅读
MJExtension源码解析

5、通过kvc修改属性值为什么会被kvo监听到

我们在上篇文章中看到通过KVC修改属性值也是可以被KVO监听到的,我们的第一个想法就是因为kvc在修改属性值的时候首先调用set方法,而遵守kvo的对象调用set方法实际上是调用中间类对象重写的set方法,在中间类重写的set方法中进而对回调方法进行了调用。这个想法是正确的,正常情况下确实是因为调用了中间类的set方法从而被监听到。

但是,根据实验我们也发现 如果通过kvc对某个对象的成员变量进行修改时 也能被kvo监听到,成员变量是没有set方法的。我的第一个想法是会不会是因为中间类的原因。因为遵守kvo的对象因为isa是指向中间类的 而中间类会重写set方法 但仔细想想也不对 因为中间类并不是随便生成的 它是根据在最初的类对象基础上重写了某些方法而已 而原始的类对象本身就没有实现set方法 因为成员变量没有set方法 所以这个原因是不成立的。那么原因就是因为kvc的内部实现中手动调用了KVO(即手动调用了willChangeValueForKey:didChangeValueForKey:方法)

KVC第4张

我们根据上面的赋值流程也知道 kvc是直接找到了变量直接进行赋值操作 那么其内部原理应该是这样的

KVC第5张

在didChangeValueForKey方法内部会调用KVO的回调方法 从而实现监听

KVC第6张KVC第7张
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 @interfaceViewController ()
68 
69 @end
70 
71 @implementationViewController
72 +(BOOL)accessInstanceVariablesDirectly {
73     returnNO;
74 }
75 
76 - (id)valueForUndefinedKey:(NSString *)key {
77     NSLog(@"出现异常,该key不存在%@",key);
78     returnnil;
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 inarrDistinct) {
143         NSLog(@"%f",age.floatValue);
144 }
145     
146     ////某一个数据中某个属性的值集合结果
147     NSArray* arrUnion = [claAry valueForKeyPath:@"@unionOfObjects.age"];
148     for (NSNumber *age inarrUnion) {
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 }
具体代码
KVC的更多功能

免责声明:文章转载自《KVC》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇JS实现跟随鼠标的魔法文字C++的关键字 explicit的作用———菜鸟级日记下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

BOS常用代码说明

下面是一些界面常用代码说明 ListUI 1)       getSelectedKeValue:单行选择时,获取当前选中行的主键值,返回ID 2)       getSelectedIdValues:多行选择时,获取选中行的主键集合,返回List 3)       checkSelected:判断是否有选中行。做业务处理的时候一般先调用一下此方法 4)...

memcpy的使用方法总结

1、memcpy 函数用于 把资源内存(src所指向的内存区域) 复制到目标内存(dest所指向的内存区域);拷贝多少个?有一个size变量控制拷贝的字节数;函数原型:void *memcpy(void *dest, void *src, unsigned int count);使用方法:(1)能够拷贝不论什么类型的对象,由于函数的參数类型是void*(...

变量的结构赋值

ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解耦; 1 数组的解耦赋值 以前,为变量赋值,只能直接指定值。 let a = 1; let b = 2; let c = 3; ES6允许写成下面这样。 let [a, b, c] = [1, 2, 3]; 上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。 这种写法属于...

OC最实用的runtime总结,面试、工作你看我就足够了!

前言 runtime的资料网上有很多了,部分有些晦涩难懂,我通过自己的学习方法总结一遍,主要讲一些常用的方法功能,以实用为主,我觉得用到印象才是最深刻的,并且最后两个demo也是MJExtension的实现原理,面试的时候也可以多扯点。 另外runtime的知识还有很多,想要了解更多可以看我翻译的官方文档(有点枯燥),本文的demo下载地址 什么是runt...

Bash变量

1、变量类型 Bash变量有两种:局部变量和环境变量 局部变量:局部变量的范围仅在创建它的Shell中有效 环境变量:能为创建它的Shell及其派生子进程所用的变量,也经常被称为全局变量,一般约定环境变量为大些,它们是那些可以通过内置命令export导出的变量。 2、变量命名 变量名必须以字母或下划线开始,其余部分则可以由字符、数字或下划线构成。变量名中不...

iOS获取所有机型

1.手机系统版本:10.3 NSString* phoneVersion = [[UIDevice currentDevice] systemVersion]; 2.手机类型:iPhone 6 NSString* phoneModel = [self iphoneType];//方法在下面 3.手机系统:iPhone OS NSString * ipone...