iOS-KVC浅谈

前言:往往会某项工具WORK,就想究其原理。
本文先简单介绍KVC

一、KVC 简介

1.1 KVC 概述

1.KVC 是 Key-Value-Coding 的简称。
2.KVC 是一种可以直接通过字符串的名字 key 来访问类属性的机制,而不是通过调用 setter、getter 方法去访问。
3.我们可以通过在运行时动态的访问和修改对象的属性。而不是在编译时确定。
4.关键方法定义在 NSKeyValueCodingProtocol。
5.基于NSKeyValueCoding非正式协议的机制。
6.NSObject是定义了KVC的,所以继承NSObject的对象都支持KVC,基本上所有的OC对象都支持KVC。
7.KVC支持类对象和内建基本数据类型。

1.2 KVC 使用

1.2.1 获取值
- (id)valueForKey:(NSString *)key;

- (id)valueForKeyPath:(NSString *)keyPath;

// 如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (id)valueForUndefinedKey:(NSString *)key;

valueForKey
[person1 setValue:@"Tom" forKey:@"name"];

valueForKeyPath
[person setValue:@"旺财" forKeyPath:@"dog.name"];

valueForUndefinedKey

//给属性赋值时,防止崩溃
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"LPAirTicketPage:%@",key);
}
1.2.2 设置值
// value的值为OC对象,如果是基本数据类型要包装成NSNumber
- (void)setValue:(id)value forKey:(NSString *)key;

// keyPath键路径,类型为xx.xx
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;

// 它的默认实现是抛出异常,可以重写这个函数做错误处理。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;

//如果在setValue方法时面给Value传nil,则会调用这个方法。
//对非类对象属性设置nil时调用,默认抛出异常。
- (void)setNilValueForKey:(NSString *)key;

// 允许直接访问实例变量,默认返回YES。如果某个类重写了这个方法,且返回NO,则KVC不可以访问该类。
+ (BOOL)accessInstanceVariablesDirectly;

// 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

// 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;

// KVC提供属性值确认的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(id)ioValue forKey:(NSString *)inKey error:(NSError)outError;
1.2.3 一对多关系成员的情况
  • mutableArrayValueForKey:有序一对多关系成员 NSArray
  • mutableSetValueForKey:无序一对多关系成员 NSSet
1.2.4 实现细节
  • setValue:forKey:
    1.首先搜索 setter 方法,有就直接赋值。
    2.如果上面的 setter 方法没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
     i.返回 NO,则执行setValue:forUNdefinedKey:
     ii.返回 YES,则按_,_的顺序搜索成员名。
    3.还没有找到的话,就调用setValue:forUndefinedKey:

简单示例:

@interface ViewController ()
@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *name;
@end

- (void)viewDidLoad {
    [super viewDidLoad];

    [self setValue:nil forKey:@"name"];
    NSLog(@"name:%@",_name);
    [self setValue:nil forKey:@"dog"];
    NSLog(@"dog");
    [self setValue:nil forKey:@"age"];
    NSLog(@"age:%d",_age);
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"===forUndefinedKey:%@",key);
}
@end

上述代码运行结果,并分析为什么?

输出结果:

name:(null)
===forUndefinedKey:dog
dog
程序会crash在给age赋值nil这里

程序分析:
因为name属性是对象,所以赋值为nil不会崩溃,对象类型可以为nil;
但age是整数,整数的类型不会是nil,强行赋值就会抛出异常出现错误;如果不小心传了nil,KVC会调用setNilValueForKey:方法,这个方法默认是抛出异常。

正确处理方法:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self setValue:nil forKey:@"name"];
    NSLog(@"name:%@",_name);
    [self setValue:nil forKey:@"dog"];
    NSLog(@"dog");
    [self setValue:nil forKey:@"age"];
    NSLog(@"age:%d",_age);
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"===forUndefinedKey:%@",key);
}
- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"%@ 赋值不能为nil",key);
}

输出结果:

name:(null)
===forUndefinedKey:dog
dog
age 赋值不能为nil
age:0

  • accessInstanceVariablesDirectly
    KVC会优先找set方法,如果找不到set方法再判断accessInstanceVariablesDirectly,如果返回NO,则不允许被KVC访问
    如下例子简单验证:
@interface ViewController ()
@property (nonatomic, assign) int age;
@end

@implementation ViewController
{
    NSString *name;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    [self setValue:@"28hs" forKey:@"age"];
    NSLog(@"age:%d",_age);
    [self setValue:@"28hs" forKey:@"name"];
    NSLog(@"name:%@",name);
}
+ (BOOL)accessInstanceVariablesDirectly{
    return NO;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"===forUndefinedKey:%@",key);
}
@end

如上代码打印结果

age:28
===forUndefinedKey:name
name:(null)

Q:为什么实现了accessInstanceVariablesDirectly,不同的变量有的可以KVC赋值有的不可以?
A: age是使用property声明,内部实现了set/get方法,所以使用KVC赋值没有问题;name变量没有实现set/get方法,找不到set方法赋值,就会判断accessInstanceVariablesDirectly,然后会导致赋值失败


  • valueForKey:
  1. 首先按get、is的顺序查找getter方法,找到直接调用。如果是bool、int等内建值类型,会做NSNumber的转换。
  2. 上面的getter没有找到,查找countOf、objectInAtIndex:、AtIndexes格式的方法。
    如果countOf和另外两个方法中的一个找到,那么就会返回一个可以响应NSArray所有方法的代理集合(collection proxy object)。发送给这个代理集合(collection proxy object)的NSArray消息方法,就会以countOf、objectInAtIndex:、AtIndexes这几个方法组合的形式调用。还有一个可选的get:range:方法。
  3. 还没查到,那么查找countOf、enumeratorOf、memberOf:格式的方法。
    如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的代理集合(collection proxy object)。发送给这个代理集合(collection proxy object)的NSSet消息方法,就会以countOf、enumeratorOf、memberOf:组合的形式调用。
  4. 还是没查到,那么如果类方法accessInstanceVariablesDirectly返回YES,那么按_,_is,is的顺序直接搜索成员名。
  5. 再没查到,调用valueForUndefinedKey:。

  • mutableArrayValueForKey
    查找有序集合成员,比如NSMutableArray
  1. 搜索insertObject:inAtIndex:、removeObjectFromAtIndex:或者insert:atIndexes、removeAtIndexes:格式的方法。
    如果至少一个insert方法和至少一个remove方法找到,那么同样返回一个可以响应NSMutableArray所有方法的代理集合。那么发送给这个代理集合的NSMutableArray消息方法,以insertObject:inAtIndex:、removeObjectFromAtIndex:、insert:atIndexes、removeAtIndexes:组合的形式调用。还有两个可选实现的接口:replaceObjectInAtIndex:withObject:、replaceAtIndexes:with:。
  2. 否则,搜索set:格式的方法,如果找到,那么发送给代理集合的NSMutableArray最终都会调用set:方法。
    也就是说,mutableArrayValueForKey取出的代理集合修改后,用set:重新赋值回去。这样做效率会差很多,所以推荐实现上面的方法。
  3. 否则,那么如果类方法accessInstanceVariablesDirectly返回YES,那么按_的顺序直接搜索成员名。如果找到,那么发送的NSMutableArray消息方法直接转交给这个成员处理。
  4. 再找不到,调用setValue:forUndefinedKey:。

  • mutableSetValueForKey:
    搜索无序集合成员,如:NSSet
  1. 搜索addObject:、removeObject:或者add:、remove:格式的方法,如果至少一个insert方法和至少一个remove方法找到,那么返回一个可以响应NSMutableSet所有方法的代理集合。那么发送给这个代理集合的NSMutableSet消息方法,以addObject:、removeObject:、add:、remove:组合的形式调用。还有两个可选实现的接口:intersect、set:。
  2. 如果reciever是ManagedObejct,那么就不会继续搜索了。
  3. 否则,搜索set:格式的方法,如果找到,那么发送给代理集合的NSMutableSet最终都会调用set:方法。也就是说,mutableSetValueForKey取出的代理集合修改后,用set:重新赋值回去。这样做效率会差很多,所以推荐实现上面的方法。
  4. 否则,那么如果类方法accessInstanceVariablesDirectly返回YES,那么按_的顺序直接搜索成员名。如果找到,那么发送的NSMutableSet消息方法直接转交给这个成员处理。
  5. 再找不到,调用setValue:forUndefinedKey:。

1.3 KVC 应用

1.3.1 字典转模型

- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;

// 定义一个字典
    NSDictionary *dict = @{
                           @"name"  : @"jack",
                           @"money" : @"20.7",
                           };
    // 创建模型
    Person *p = [[Person alloc] init];
    
    // 字典转模型
    [p setValuesForKeysWithDictionary:dict];
    NSLog(@"person's name is the %@",p.name);
1.3.2 集合类操作符

如果一个对象包含一个数组或者是集合的属性那么使用valueForKeyPath获取相关的属性时可以在键的路径中插入一些函数。这些函数称为集合操作符。
集合运算符是一个特殊的Key Path,可以作为参数传递给valueForKeyPath:方法,注意只能是这个方法,如果传给了valueForKey:方法保证你程序崩溃。

集合操作符分类:

  • 简单的集合操作符
    @avg:遍历集合中的元素将它们转换为一个双精度浮点数并返回表示它们的平均数NSNumber类型的对象
    @count:集合中对象的数量
    @max:集合中的最大值
    @min:集合中的最小值
    @sum:遍历集合中的每一项将它们转换为一个双精度浮点数并返回表示它们的和NSNumber类型的对象

  • 对象操作符
    @distinctUnionOfObjects:返回集合中所有的对象如果有相同的对象那么只返回一个。
    @unionOfObjects:与@distinctUnionOfObjects相反,返回所有的对象包括重复的对象。

  • 数组和集合操作符
    @distinctUnionOfArrays:返回指定属性去重后的值的数组
    @unionOfArrays:返回指定属性的值的数组,不去重
    @distinctUnionOfSets:同上,只是返回值为NSSet
    @distinctUnionOfArrays、@unionOfArrays、@distinctUnionOfSets上述使用方法大致相同只不过操作对象由数组里的对象变成数组里的集合。

操作符使用格式

代码示例

//Person.h
#import 
@interface Person : NSObject
{
    @private
    NSArray *grades;
}
@property (nonatomic,strong) NSString *name;
@property (nonatomic,assign) NSInteger age;
@property (nonatomic,strong) NSArray *subjects;
-(instancetype)init;
@end
//Person.m
#import "Person.h"

@implementation Person
-(instancetype)init{
    self= [super init];
    if(self) {
        _subjects = @[@"Math",@"Chinese",@"science",@"Code"];
    }
    return self;
}
@end

//外部调用
- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p1 = [[Person alloc]init];
    p1.name = @"Tom";
    p1.age = 18;
    [p1 setValue:@[
                   @{@"grade":@90},
                   @{@"grade":@56},
                   @{@"grade":@76},
                   @{@"grade":@98},
                   @{@"grade":@98},
                   ]
          forKey:@"grades"];
    
    NSLog(@"Subject:%@",p1.subjects);
    NSLog(@"Grades:%@",[p1 valueForKey:@"grades"]);
    
    id obj1 = [p1 valueForKeyPath:@"[email protected]"];
    NSLog(@"avg:%@--class:%@",obj1,[obj1 class]);
    NSLog(@"count:%@",[p1 valueForKeyPath:@"[email protected]"]);
    NSLog(@"max:%@",[p1 valueForKeyPath:@"[email protected]"]);
    NSLog(@"min:%@",[p1 valueForKeyPath:@"[email protected]"]);
    NSLog(@"sum:%@",[p1 valueForKeyPath:@"[email protected]"]);
NSLog(@"obj:%@",[p1 valueForKeyPath:@"[email protected]"]);
NSLog(@"obj:%@",[p1 valueForKeyPath:@"[email protected]"])
}

运行结果

[3474:184773] Subject:(
    Math,
    Chinese,
    science,
    Code
)
[3474:184773] Grades:(
        {
        grade = 90;
    },
        {
        grade = 56;
    },
        {
        grade = 76;
    },
        {
        grade = 98;
    }
[3474:184773] avg:80--class:NSDecimalNumber
[3474:184773] count:4
[3474:184773] max:98
[3474:184773] min:56
[3474:184773] sum:320
[3667:204228] obj:(
    56,
    98,
    90,
    76
)//因为数组里有2个98,返回一个
[3667:204228] obj:(
    56,
    98,
    90,
    76,
    98
)
1.3.3 修改私有属性

1.修改 TextField 的 placeholder:

[_textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];   

[_textField setValue:[UIFont systemFontOfSize:14] forKeyPath:@“_placeholderLabel.font"];

2.修改 UIPageControl 的图片:

[_pageControl setValue:[UIImage imageNamed:@"selected"] forKeyPath:@"_currentPageImage"];

[_pageControl setValue:[UIImage imageNamed:@"unselected"] forKeyPath:@"_pageImage"];
1.3.4 一对多关系(To-Many)中的集合访问器方法

平时大部分使用的属性都是一对一关系(To-One),比如Person类中的name属性,每个人只有一个名字。但也有一对多的关系,比如Person中有一个friendsName属性,这是个集合(在Objective-C中可以是NSArray,NSSet等),保存的是一个人的所有朋友的名字。
当操作一对多的属性中的内容时,我们有两种选择:
①间接操作
先通过KVC方法取到集合属性,然后通过集合属性操作集合中的元素。
②直接操作
苹果为我们提供了一些方法模板,我们可以以规定的格式实现这些方法来达到访问集合属性中元素的目的。

有序集合对应方法如下:

-countOf  
//必须实现,对应于NSArray的基本方法count:  
-objectInAtIndex:  
-AtIndexes:  
//这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:  
-get:range:  
//不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:  
  
-insertObject:inAtIndex:  
-insert:atIndexes:  
//两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:  
-removeObjectFromAtIndex:  
-removeAtIndexes:  
//两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:  
-replaceObjectInAtIndex:withObject:  
-replaceAtIndexes:with:  
//可选的,如果在此类操作上有性能问题,就需要考虑实现之  

无序集合对应方法如下:

-countOf  
//必须实现,对应于NSArray的基本方法count:  
-objectInAtIndex:  
-AtIndexes:  
//这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:  
-get:range:  
//不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:  
  
-insertObject:inAtIndex:  
-insert:atIndexes:  
//两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:  
-removeObjectFromAtIndex:  
-removeAtIndexes:  
//两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:  
-replaceObjectInAtIndex:withObject:  
-replaceAtIndexes:with:  
//这两个都是可选的,如果在此类操作上有性能问题,就需要考虑实现之 
1.3.5 键值验证(Key-Value Validation)

KVC是不会自动调用键值验证方法的,就是说我们需要手动验证。
KVC提供了验证Key对应的Value是否可用的方法:

- (BOOL)validateValue:(inout id *)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;  
//该方法默认的实现是调用一个如下格式的方法:
- (BOOL)validate:error: 

示例代码:

-(BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError {  
    // Implementation specific code.  
    return ...;  
} 
1.3.6 KVC对数值和结构体型属性的支持

一套机制如果不支持数值和结构体型的数据,那么它的实用性就会大大折扣。KVC可以自动的将数值或结构体型的数据打包或解包成NSNumber或NSValue对象,以达到适配的目的。
①修改值,我们通过KVC技术使用setValue forkey设置age属性的值。
KVC会自动的将NSNumber对象转换成NSInteger对象,然后再调用相应的访问器方法设置age的值。
②获取值,valueForKey获取age属性值。会以NSNumber的形式返回age的值。
③使用NSNumber封装,可以使用NSNumber的数据类型有:

+ (NSNumber *)numberWithChar:(char)value;  
+ (NSNumber *)numberWithUnsignedChar:(unsigned char)value;  
+ (NSNumber *)numberWithShort:(short)value;  
+ (NSNumber *)numberWithUnsignedShort:(unsigned short)value;  
+ (NSNumber *)numberWithInt:(int)value;  
+ (NSNumber *)numberWithUnsignedInt:(unsigned int)value;  
+ (NSNumber *)numberWithLong:(long)value;  
+ (NSNumber *)numberWithUnsignedLong:(unsigned long)value;  
+ (NSNumber *)numberWithLongLong:(long long)value;  
+ (NSNumber *)numberWithUnsignedLongLong:(unsigned long long)value;  
+ (NSNumber *)numberWithFloat:(float)value;  
+ (NSNumber *)numberWithDouble:(double)value;  
+ (NSNumber *)numberWithBool:(BOOL)value;  
+ (NSNumber *)numberWithInteger:(NSInteger)value NS_AVAILABLE(10_5, 2_0);  
+ (NSNumber *)numberWithUnsignedInteger:(NSUInteger)value NS_AVAILABLE(10_5, 2_0);  

④使用NSValue封装,NSValue主要用于处理结构体型的数据,它本身提供了如下集中结构的支持:

+ (NSValue *)valueWithCGPoint:(CGPoint)point;  
+ (NSValue *)valueWithCGSize:(CGSize)size;  
+ (NSValue *)valueWithCGRect:(CGRect)rect;  
+ (NSValue *)valueWithCGAffineTransform:(CGAffineTransform)transform;  
+ (NSValue *)valueWithUIEdgeInsets:(UIEdgeInsets)insets;  
+ (NSValue *)valueWithUIOffset:(UIOffset)insets NS_AVAILABLE_IOS(5_0);  

二、KVC 总结

2.1 KVC 与点语法比较

用 KVC 访问属性和用点语法访问属性的区别:

  1. 用点语法编译器会做预编译检查,访问不存在的属性编译器会报错,但是用 KVC 方式编译器无法做检查,如果有错误只能运行的时候才能发现(crash)。
  2. 相比点语法用 KVC 方式 KVC 的效率会稍低一点,但是灵活,可以在程序运行时决定访问哪些属性。
  3. 用 KVC 可以访问对象的私有成员变量。

2.2 KVC优缺点

键值编码是一种间接访问对象的属性使用字符串来标识属性,而不是通过调用存取方法直接或通过实例变量访问的机制,非对象类型的变量将被自动封装或者解封成对象,很多情况下会简化程序代码。

优点:

  • 不需要通过 setter、getter 方法去访问对象的属性,可以访问对象的私有属性
  • 可以轻松处理集合类(NSArray)。
  • 这种统一的直接通过字符串存取ObjC中对象的成员属性的接口,可以实现由外部脚本控件程序执行或者获取程序执行信息。
  • 通过KVC存取二进制库中的私有成员也比较实用。

缺点:

  • 一旦使用KVC你的编译器无法检查出错误,即不会对设置的键、键值路径进行错误检查。
  • 执行效率要低于 setter 和 getter 方法。因为使用 KVC 键值编码,它必须先解析字符串,然后在设置或者访问对象的实例变量。
  • 使用 KVC 会破坏类的封装性。

三、KVC 原理

KVC 运用了一个isa-swizzling技术。isa-swizzling就是类型混合指针机制。KVC主要通过isa-swizzling,来实现其内部查找定位的。isa指针,如其名称所指,(就是is a kind of的意思),指向维护分发表的对象的类。该分发表实际上包含了指向实现类中的方法的指针,和其它数据。主要依据runtime的强大动态能力。

比如说如下的一行KVC的代码:
[site setValue:@"sitename" forKey:@"name"];

就会被编译器处理成:

SEL sel = sel_get_uid ("setValue:forKey:");
IMP method = objc_msg_lookup (site->isa,sel);
method(site, sel, @"sitename", @"name");

基本概念:

  • SEL数据类型:它是编译器运行Objective-C里的方法的环境参数,查找方法表时所用的键。定义成char*,实质上可以理解成int值。
  • IMP数据类型:他其实就是一个编译器内部实现时候的函数指针。当Objective-C编译器去处理实现一个方法的时候,就会指向一个IMP对象,这个对象是C语言表述的类型。

KVC 再某种程度上提供了访问器的替代方案。访问器方法是一个很好的东西,以至于只要是有可能,KVC也尽量再访问器方法的帮助下工作。为了设置或者返回对象属性。

KVC按顺序使用如下技术:
a.检查是否存在名为-set:的方法,并使用它做设置值。对于-get和-set:方法,将大写Key字符串的第一个字母,并与Cocoa的方法命名保持一致;
b.如果上述方法不可用,则检查名为_、_is(只针对布尔值有效)、_getset方法;
c.如果没有找到访问器方法,可以尝试直接访问实例变量。实例变量可以是名为:
;
d.如果仍为找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。

KVC的内部机制:
一个对象在调用setValue的时候进行了如下操作:
(1)根据方法名找到运行方法的时候需要的环境参数
(2)他会从自己的isa指针结合环境参数,找到具体的方法实现接口。
(3)再直接查找得来的具体的实现方法

Q:setValue:forKey:valueForKey:的原理

iOS-KVC浅谈_第1张图片
setValue:forKey:的原理

iOS-KVC浅谈_第2张图片
valueForKey:的原理

注:setKey:_setKey是按顺序查找方法;_key_isKeykeyisKey是按顺序查找成员变量

下一篇:iOS-KVO浅谈

你可能感兴趣的:(iOS-KVC浅谈)