KVC
一、 KVC 介绍
KVC也就是key-value-coding,即键值编码,说白了就是通过指定的key获得想要的值value。而不是通过调用setter
、getter
方法访问。
二、 KVC 的强大之处
1、访问私有变量
一个类中的私有变量,不能直接通过setter
、getter
方法访问。
但是却可以通过KVC来访问。举例:
一个类如下:
@interface Dog : NSObject
{
@private
double height;
}
这里的私有变量 height
,假如直接使用setter
、getter
方法访问,就会出现下面的错误。
这时候可以使用KVC来访问这个私有变量:
Dog *dog = [[Dog alloc]init];
[dog setValue:@12 forKey:@"height"];//给私有变量赋值
//读取私有变量的值
NSLog(@"dog's height is = %@",[dog valueForKey:@"height"]);
例子
实际使用举例:
利用kvc的这个特性,我们可以访问系统里的一些私有变量。
例如:在UIPageControl
里面有两个私有变量:
UIImage* _currentPageImage;
UIImage* _pageImage;
我们可以通过kvc
来进行读取和赋值:
UIPageControl *pageControl = [[UIPageControl alloc]init];
//设置值
[pageControl setValue:[UIImage imageNamed:@"XX"] forKeyPath:@"_currentPageImage"];
[pageControl setValue:[UIImage imageNamed:@"XX"] forKeyPath:@"_pageImage"];
//读取值
UIImage *currentImage = [pageControl valueForKey:@"_currentPageImage"];
UIImage *pageImage = [pageControl valueForKey:@"_pageImage"];
这样我们就可以设置当前显示和未显示到脚标的样式了。
2、Key和KeyPath
KVC 定义了一种按名称访问对象属性的机制,支持这种访问的主要方法是:
- (id)valueForKey:(NSString *)key;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
前边两个方法用到的Key较容易理解,就是要访问的属性名称对应的字符串。
后面两个方法用到的KeyPath
是一个被点操作符隔开的用于访问对象的指定属性的字符串序列。
比如KeyPath address.street
将会访问消息接收对象所包含的address
属性中包含的一个street
属性。其实KeyPath
说白了就是我们平时使用点操作访问某个对象的属性时所写的那个字符串。
Q: 在【1、访问私有变量】上面代码中,我们使用了
[dog setValue:@12 forKey:@"height"];//给私有变量赋值
在UIPageControl中,使用了
[pageControl setValue:[UIImage imageNamed:@"XX"] forKeyPath:@"_currentPageImage"];
为什么后来使用了 setValue:for keyPath 呢?
A: 如果我们使用了下面这种写法,
p.dog = [[Dog alloc] init];
[p setValue:@200 forKey:@"dog.weight"];
如果我们直接这样是会报错说找不到dog.weight
这个key
的,而在storyboard中,我们拖控件连线错误的时候也会报错说找不到什么key,说明storyboard在赋值的时候也是通过kvc来操作的。
这里如果我们换另外的一个方法,这时候是不会报错的,而且可以打印出狗的体重.
[p setValue:@200 forKeyPath:@"dog.weight"];
说明forKeyPath
是包含了forKey
这个方法的功能的,甚至forKeyPath
方法还有它自己的高级的功能,它会先去找有没有dog这个key,然后去找有没有weight这个属性。所以我们在使用kvc的时候,最好用forKeyPath
这个方法。
3、一对多关系(To-Many)中的集合访问器方法
我们平时大部分使用的属性都是一对一关系(To-One),比如Person类中的name属性,每个人只有一个名字。但也有一对多的关系,比如Person中有一个friendsName属性,这是个集合(在Objective-C中可以是NSArray,NSSet等),保存的是一个人的所有朋友的名字。
当操作一对多的属性中的内容时,我们有两种选择:
①间接操作
先通过KVC方法取到集合属性,然后通过集合属性操作集合中的元素。
②直接操作
苹果为我们提供了一些方法模板,我们可以以规定的格式实现这些方法来达到访问集合属性中元素的目的。
有序集合对应方法如下:
-countOf<Key>
//必须实现,对应于NSArray的基本方法count:
-objectIn<Key>AtIndex:
-<key>AtIndexes:
//这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
-get<Key>:range:
//不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:
-insertObject:in<Key>AtIndex:
-insert<Key>:atIndexes:
//两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
-removeObjectFrom<Key>AtIndex:
-remove<Key>AtIndexes:
//两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
-replaceObjectIn<Key>AtIndex:withObject:
-replace<Key>AtIndexes:with<Key>:
//可选的,如果在此类操作上有性能问题,就需要考虑实现之
无序集合对应方法如下:
-countOf<Key>
//必须实现,对应于NSArray的基本方法count:
-objectIn<Key>AtIndex:
-<key>AtIndexes:
//这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
-get<Key>:range:
//不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:
-insertObject:in<Key>AtIndex:
-insert<Key>:atIndexes:
//两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
-removeObjectFrom<Key>AtIndex:
-remove<Key>AtIndexes:
//两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
-replaceObjectIn<Key>AtIndex:withObject:
-replace<Key>AtIndexes:with<Key>:
//这两个都是可选的,如果在此类操作上有性能问题,就需要考虑实现之
不过这些方法除非是很有需求,否则个人认为没有实现的必要,间接法也不是很麻烦,基本能满足需求了。值得指出的是,苹果甚至都没有让这些方法以哪怕是非正式协议的形式出现,而只是在编程指南中提了一下。
4、键值验证(Key-Value Validation)
KVC提供了验证Key对应的Value是否可用的方法:
- (BOOL)validateValue:(inout id *)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
该方法默认的实现是调用一个如下格式的方法:
- (BOOL)validate<Key>:error:
比如属性name对应的方法为:
-(BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError {
// Implementation specific code.
return ...;
}
这样就给了我们一次纠错的机会。
需要指出的是,KVC是不会自动调用键值验证方法的,就是说我们需要手动验证。但是有些技术,比如CoreData会自动调用。
5、KVC对数值和结构体型属性的支持
一套机制如果不支持数值和结构体型的数据,那么它的实用性就会大大折扣。
幸运的是KVC中苹果对这方面的支持做的很好。
KVC可以自动的将数值或结构体型的数据打包或解包成NSNumber或NSValue对象,以达到适配的目的。
举个例子,Person
类有个个NSInteger
类型的age
属性
①修改值
我们通过KVC技术使用如下方式设置age属性的值:
[person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];
我们赋给age
的是一个NSNumber
对象,KVC会自动的将NSNumber
对象转换成NSInteger
对象,然后再调用相应的访问器方法设置age
的值。
②获取值
同样,以如下方式获取age
属性值:
[person valueForKey:@"age"];
这时,会以NSNumber
的形式返回age
的值。
需要说明的是,什么时候返回的是NSNumber
,什么时候返回的是NSValue
?
③使用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);
只有有限的6种而已!
那对于其它自定义的结构体怎么办?别担心,任何结构体都是可以转化成NSValue对象的,具体实现方法参见文章:
http://blog.csdn.net/wzzvictory/article/details/8614433
6、集合运算符(Collection Operators)
集合运算符是一个特殊的KeyPath
,可以作为参数传递给valueForKeyPath:
方法,注意只能是这个方法,如果传给了valueForKey:
方法保证你程序崩溃。
运算符是一个以@开头的特殊字符串,格式如下图所示:
①简单集合运算符
简单集合运算符共有@avg
,@count
,@max
,@min
,@sum
5种,都表示啥不用我说了吧,目前还不支持自定义。
有一个集合类的对象:transactions
,它存储了一个个的Transaction
类的实例,该类有三个属性:payee
,amount
,date
。下面以此为例说明如何使用这些运算符:
要获取amount
的平均值可以这样:
NSNumber *transactionAverage = [transactions valueForKeyPath:@"@avg.amount"];
要获取transactions
集合中元素数目可以这样:
NSNumber *numberOfTransactions = [transactions valueForKeyPath:@"@count"];
需要之处的是,@count
是这些集合运算符中比较特殊的一个,因为它没有右路经,原因很容易理解。
②对象运算符
比集合运算符稍微复杂,能以数组的方式返回指定的内容,一共有两种:
@distinctUnionOfObjects
@unionOfObjects
它们的返回值都是NSArray
,
区别是前者返回的元素都是唯一的,是去重以后的结果;
后者返回的元素是全集。
用法如下:
NSArray *payees = [transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
NSArray *payees = [transactions valueForKeyPath:@"@unionOfObjects.payee"];
前者会将收款人的姓名去除重复的以后返回,后者直接返回所有收款人的姓名。
③Array和Set操作符
这种情况更复杂了,说的是集合中包含集合的情况,我们执行了如下的一段代码:
// Create the array that contains additional arrays.
self.arrayOfTransactionsArray = [NSMutableArray array];
// Add the array of objects used in the above examples.
[self.arrayOfTransactionsArray addObject:transactions];
// Add a second array of objects; this array contains alternate values.
[self.arrayOfTransactionsArrays addObject:moreTransactions];
得到了一个包含集合的集合:arrayOfTransactionsArray
这时如果我们想操作arrayOfTransactionsArray
中包含的集合中的元素时,可以使用如下三个运算符:
@distinctUnionOfArrays
@unionOfArrays
@distinctUnionOfSets
前两个针对的集合是Arrays,后一个针对的集合是Sets。
因为Sets中的元素本身就是唯一的,所以没有对应的@unionOfSets
运算符。
它们的用法举例如下:
NSArray *payees = [arrayOfTransactionsArrays valueForKeyPath:@"@unionOfArrays.payee"];
7、 使用KVC直接访问NSArray 或者 NSSet 的属性值
NSArray/NSSet等都支持KVC,这里举一个例子:
其中,book是一个书类,有一个属性是name,一个属性是price。
NSArray *books= @[book1, book2, book3];
NSArray *names = [books valueForKeyPath:@"name"];
//使用kvc直接打印出来书的平均价格
NSLog(@"%@", [books valueForKeyPath:@"@avg.price"]);
8、使用KVC将字典(json)转化成模型
// 定义一个字典
NSDictionary *dict = @{
@"name" : @"jack",
@"money" : @"20.7",
};
// 创建模型
Person *p = [[Person alloc] init];
// 字典转模型
[p setValuesForKeysWithDictionary:dict];
NSLog(@"person's name is the %@",p.name);
}
注意:
(1). key的值必须正确,如果拼写错误,会出现异常
(2). 当key的值是没有定义的,valueForUndefinedKey:这个方法会被调用,如果你自己写了这个方法,key的值出错就会调用到这里来
(3). 因为类key反复嵌套,所以有个keyPath的概念,keyPath就是用.号来把一个一个key链接起来,这样就可以根据这个路径访问下去
(4). NSArray/NSSet等都支持KVC
KVO
当一个类的属性被观察的时候,系统会通过runtime
动态的创建一个该类的派生类,并且会在这个类中重写基类被观察的属性的setter
方法,而且系统将这个类的isa
指针指向了派生类,从而实现了给监听的属性赋值时调用的是派生类的setter
方法。
重写的setter
方法会在调用原setter
方法前后,通知观察对象值得改变。
NSKVONotifying_Person 是新创建出来新类。
一、KVO介绍
KVO
就是观察者模式。
说白了就是你关心的一个值改变了,你就会得到通知。你就可以在你想处理的地方处理这个值。
二、KVO的使用
一般分为三步:
1、注册监听
使用方法:
/**
* 添加KVO监听者
*
* @param observer 观察者(监听器)
* @param keyPath 属性名(要观察的属性)
* @param options
* @param context 传递的参数
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
例子:
//为对象p添加一个观察者(监听器)
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:@"test"];
2、设置监听事件
/**
* 当利用KVO监听到某个对象的属性值发生了改变,就会自动调用这个
*
* @param keyPath 哪个属性被改了
* @param object 哪个对象的属性被改了
* @param change 改成咋样
* @param context 当初addObserver时的context参数值
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"%@ %@ %@ %@", object, keyPath, change, context);
}
3、取消监听
可以在任意位置取消监听,试逻辑而定。
-(void)dealloc
{
[p removeObserver:self forKeyPath:@"name"];
}
实现原理
1、KVC如何访问属性值
KVC再某种程度上提供了访问器的替代方案。
不过访问器方法是一个很好的东西,以至于只要是有可能,KVC也尽量再访问器方法的帮助下工作。
为了设置或者返回对象属性,KVC按顺序使用如下技术:
①检查是否存在-
、-is
(只针对布尔值有效)或者-get
的访问器方法,如果有可能,就是用这些方法返回值;
检查是否存在名为-set:
的方法,并使用它做设置值。对于-get
和-set:
方法,将大写Key字符串的第一个字母,并与Cocoa
的方法命名保持一致;
②如果上述方法不可用,则检查名为-_
、-_is
(只针对布尔值有效)、-_get
和-_set:
方法;
③如果没有找到访问器方法,可以尝试直接访问实例变量。实例变量可以是名为:
或_
;
④如果仍未找到,则调用valueForUndefinedKey:
和setValue:forUndefinedKey:
方法。这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。
2、KVC/KVO实现原理
键值编码和键值观察是根据isa-swizzling
技术来实现的,主要依据runtime
的强大动态能力。下面的这段话是引自网上的一篇文章:
http://blog.csdn.net/kesalin/article/details/8194240
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。
派生类在被重写的setter
方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter
方法,而通过重写就获得了 KVO
需要的通知机制。当然前提是要通过遵循 KVO
的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO
的。
同时派生类还重写了 class
方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的isa
指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对setter
的调用就会调用重写的setter
,从而激活键值通知机制。此外,派生类还重写了dealloc
方法来释放资源。
在一篇介绍Objective-C类和元类的文章:
http://blog.csdn.net/wzzvictory/article/details/8592492中介绍过,
isa指针指向的其实是类的元类,如果之前的类名为:Person
,那么被runtime
更改以后的类名会变成:NSKVONotifying_Person
。
新的NSKVONotifying_Perso
n类会重写以下方法:增加了监听的属性对应的set
,class
,dealloc
,_isKVOA
。
①class
重写class
方法是为了我们调用它的时候返回跟重写继承类之前同样的内容。
打印如下内容:
NSLog(@"self->isa:%@",self->isa);
NSLog(@"self class:%@",[self class]);
在建立KVO
监听前,打印结果为:
self->isa:Person
self class:Person
在建立KVO
监听之后,打印结果为:
self->isa:NSKVONotifying_Person
self class:Person
这也是isa
指针和class
方法的一个区别,大家使用的时候注意。
②重写set方法
新类会重写对应的set
方法,是为了在set
方法中增加另外两个方法的调用:
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
其中,didChangeValueForKey:
方法负责调用:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
方法,这就是KVO实现的原理了!
如果没有任何的访问器方法,-setValue:forKey
方法会直接调用:
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
如果在没有使用键值编码且没有使用适当命名的访问起方法的时候,我们只需要显示调用上述两个方法,同样可以使用KVO!
总结一下,想使用KVO有三种方法:
1)使用了KVC
使用了KVC,如果有访问器方法,则运行时会在访问器方法中调用will/didChangeValueForKey:方法;
没用访问器方法,运行时会在setValue:forKey方法中调用will/didChangeValueForKey:方法。
2)有访问器方法
运行时会重写访问器方法调用will/didChangeValueForKey:方法。
因此,直接调用访问器方法改变属性值时,KVO也能监听到。
3)显示调用will/didChangeValueForKey:方法。
总之,想使用KVO,只要有will/didChangeValueForKey:方法就可以了。
_isKVOA
这个私有方法估计是用来标示该类是一个 KVO 机制声称的类。
优点和缺点
1、优点
①可以再很大程度上简化代码
例子网上很多,这就不举了
②能跟脚本语言很好的配合
才疏学浅,没学过AppleScript等脚本语言,所以没能深刻体会到该优点。
2、缺点
KVC的缺点不明显,主要是KVO的,详情可以参考这篇文章:
http://www.mikeash.com/pyblog/key-value-observing-done-right.html
核心思想是说KVO
的回调机制,不能传一个selector
或者block
作为回调,而必须重写-addObserver:forKeyPath:options:context:
方法所引发的一系列问题。
为了解决这个问题,作者还亲自实现了一个MAKVONotificationCenter
类,代码见github
:
https://github.com/mikeash/MAKVONotificationCenter
不过个人认为这只是苹果做的KVO不够完美,不能算是缺陷。
KVO 和 KVC 的区别和用法,干货!!! 传送门
KVC和KVO 传送门
KVC/KVO原理详解及编程指南
[深入浅出Cocoa]详解键值观察(KVO)及其实现机理