热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

KVC/KVO原理详解及编程指南(转载)

KVCKVO原理详解及编程指南作者:wangzz原文地址:http:blog.csdn.netwzzvictoryarticledetails9674431转载请注明出处如果觉得文
KVC/KVO原理详解及编程指南
作者:wangzz
原文地址:http://blog.csdn.net/wzzvictory/article/details/9674431
转载请注明出处
如果觉得文章对你有所帮助,请通过留言或关注微信公众帐号wangzzstrive来支持我,谢谢!
 
前言:
1、本文基本不讲KVC/KVO的用法,只结合网上的资料说说对这种技术的理解。
2、由于KVO内容较少,而且是以KVC为基础实现的,本文将着重介绍KVC部分。

一、简介

KVC/KVO是观察者模式的一种实现,在Cocoa中是以被万物之源NSObject类实现的NSKeyValueCoding/NSKeyValueObserving非正式协议的形式被定义为基础框架的一部分。从协议的角度来说,KVC/KVO本质上是定义了一套让我们去遵守和实现的方法。
当然,KVC/KVO实现的根本是Objective-C的动态性和runtime,这在后文的原理部分会有详述。
另外,KVC/KVO机制离不开访问器方法的实现,这在后文中也有解释。

1、KVC简介

全称是Key-value coding,翻译成键值编码。顾名思义,在某种程度上跟map的关系匪浅。它提供了一种使用字符串而不是访问器方法去访问一个对象实例变量的机制。

2、KVO简介

全称是Key-value observing,翻译成键值观察。提供了一种当其它对象属性被修改的时候能通知当前对象的机制。再MVC大行其道的Cocoa中,KVO机制很适合实现model和controller类之间的通讯。

二、KVC相关技术

1、Key和Key Path

KVC定义了一种按名称访问对象属性的机制,支持这种访问的主要方法是:
[java] view plaincopy
 
  1. - (id)valueForKey:(NSString *)key;  
  2. - (void)setValue:(id)value forKey:(NSString *)key;  
  3. - (id)valueForKeyPath:(NSString *)keyPath;  
  4. - (void)setValue:(id)value forKeyPath:(NSString *)keyPath;  
前边两个方法用到的Key较容易理解,就是要访问的属性名称对应的字符串。
后面两个方法用到的KeyPath是一个被点操作符隔开的用于访问对象的指定属性的字符串序列。比如KeyPath address.street将会访问消息接收对象所包含的address属性中包含的一个street属性。其实KeyPath说白了就是我们平时使用点操作访问某个对象的属性时所写的那个字符串。

2、点语法和KVC

在实现了访问器方法的类中,使用点语法和KVC访问对象其实差别不大,二者可以任意混用。但是没有访问起方法的类中,点语法无法使用,这时KVC就有优势了。(原因见第三部分的第一节:KVC如何访问属性值。)

3、一对多关系(To-Many)中的集合访问器方法

我们平时大部分使用的属性都是一对一关系(To-One),比如Person类中的name属性,每个人只有一个名字。但也有一对多的关系,比如Person中有一个friendsName属性,这是个集合(在Objective-C中可以是NSArray,NSSet等),保存的是一个人的所有朋友的名字。
当操作一对多的属性中的内容时,我们有两种选择:
①间接操作
先通过KVC方法取到集合属性,然后通过集合属性操作集合中的元素。
②直接操作
苹果为我们提供了一些方法模板,我们可以以规定的格式实现这些方法来达到访问集合属性中元素的目的。
有序集合对应方法如下:
[java] view plaincopy
 
  1. -countOf  
  2. //必须实现,对应于NSArray的基本方法count:  
  3. -objectInAtIndex:  
  4. -AtIndexes:  
  5. //这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:  
  6. -get:range:  
  7. //不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:  
  8.   
  9. -insertObject:inAtIndex:  
  10. -insert:atIndexes:  
  11. //两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:  
  12. -removeObjectFromAtIndex:  
  13. -removeAtIndexes:  
  14. //两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:  
  15. -replaceObjectInAtIndex:withObject:  
  16. -replaceAtIndexes:with:  
  17. //可选的,如果在此类操作上有性能问题,就需要考虑实现之  
无序集合对应方法如下:
[java] view plaincopy
 
  1. -countOf  
  2. //必须实现,对应于NSArray的基本方法count:  
  3. -objectInAtIndex:  
  4. -AtIndexes:  
  5. //这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:  
  6. -get:range:  
  7. //不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:  
  8.   
  9. -insertObject:inAtIndex:  
  10. -insert:atIndexes:  
  11. //两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:  
  12. -removeObjectFromAtIndex:  
  13. -removeAtIndexes:  
  14. //两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:  
  15. -replaceObjectInAtIndex:withObject:  
  16. -replaceAtIndexes:with:  
  17. //这两个都是可选的,如果在此类操作上有性能问题,就需要考虑实现之  
不过这些方法除非是很有需求,否则个人认为没有实现的必要,间接法也不是很麻烦,基本能满足需求了。值得指出的是,苹果甚至都没有让这些方法以哪怕是非正式协议的形式出现,而只是在编程指南中提了一下。

4、键值验证(Key-Value Validation)

KVC提供了验证Key对应的Value是否可用的方法:
[java] view plaincopy
 
  1. - (BOOL)validateValue:(inout id *)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;  
该方法默认的实现是调用一个如下格式的方法:
[java] view plaincopy
 
  1. - (BOOL)validate:error:  
比如属性name对应的方法为:
[java] view plaincopy
 
  1. -(BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError {  
  2.     // Implementation specific code.  
  3.     return ...;  
  4. }  
这样就给了我们一次纠错的机会。
需要指出的是,KVC是不会自动调用键值验证方法的,就是说我们需要手动验证。但是有些技术,比如CoreData会自动调用。

5、KVC对数值和结构体型属性的支持

一套机制如果不支持数值和结构体型的数据,那么它的实用性就会大大折扣。幸运的是KVC中苹果对这方面的支持做的很好。KVC可以自动的将数值或结构体型的数据打包或解包成NSNumber或NSValue对象,以达到适配的目的。
举个例子,Person类有个个NSInteger类型的age属性
①修改值
我们通过KVC技术使用如下方式设置age属性的值:
[java] view plaincopy
 
  1. [person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];  
我们赋给age的是一个NSNumber对象,KVC会自动的将NSNumber对象转换成NSInteger对象,然后再调用相应的访问器方法设置age的值。
②获取值
同样,以如下方式获取age属性值:
[java] view plaincopy
 
  1. [person valueForKey:@"age"];  
这时,会以NSNumber的形式返回age的值。
需要说明的是,什么时候返回的是NSNumber,什么时候返回的是NSValue?
③使用NSNumber封装
可以使用NSNumber的数据类型有:
[java] view plaincopy
 
  1. + (NSNumber *)numberWithChar:(char)value;  
  2. + (NSNumber *)numberWithUnsignedChar:(unsigned char)value;  
  3. + (NSNumber *)numberWithShort:(short)value;  
  4. + (NSNumber *)numberWithUnsignedShort:(unsigned short)value;  
  5. + (NSNumber *)numberWithInt:(int)value;  
  6. + (NSNumber *)numberWithUnsignedInt:(unsigned int)value;  
  7. + (NSNumber *)numberWithLong:(long)value;  
  8. + (NSNumber *)numberWithUnsignedLong:(unsigned long)value;  
  9. + (NSNumber *)numberWithLongLong:(long long)value;  
  10. + (NSNumber *)numberWithUnsignedLongLong:(unsigned long long)value;  
  11. + (NSNumber *)numberWithFloat:(float)value;  
  12. + (NSNumber *)numberWithDouble:(double)value;  
  13. + (NSNumber *)numberWithBool:(BOOL)value;  
  14. + (NSNumber *)numberWithInteger:(NSInteger)value NS_AVAILABLE(10_5, 2_0);  
  15. + (NSNumber *)numberWithUnsignedInteger:(NSUInteger)value NS_AVAILABLE(10_5, 2_0);  
总之就是一些常见的数值型数据。
④使用NSValue封装
NSValue主要用于处理结构体型的数据,它本身提供了如下集中结构的支持:
[java] view plaincopy
 
  1. + (NSValue *)valueWithCGPoint:(CGPoint)point;  
  2. + (NSValue *)valueWithCGSize:(CGSize)size;  
  3. + (NSValue *)valueWithCGRect:(CGRect)rect;  
  4. + (NSValue *)valueWithCGAffineTransform:(CGAffineTransform)transform;  
  5. + (NSValue *)valueWithUIEdgeInsets:(UIEdgeInsets)insets;  
  6. + (NSValue *)valueWithUIOffset:(UIOffset)insets NS_AVAILABLE_IOS(5_0);  
只有有限的6种而已!那对于其它自定义的结构体怎么办?别担心,任何结构体都是可以转化成NSValue对象的,具体实现方法参见我之前的一篇文章:
http://blog.csdn.net/wzzvictory/article/details/8614433

6、集合运算符(Collection Operators)

集合运算符是一个特殊的Key Path,可以作为参数传递给valueForKeyPath:方法,注意只能是这个方法,如果传给了valueForKey:方法保证你程序崩溃。
运算符是一个以@开头的特殊字符串,格式如下图所示:
技术分享
①简单集合运算符
简单集合运算符共有@avg,@count,@max,@min,@sum5种,都表示啥不用我说了吧,目前还不支持自定义。
有一个集合类的对象:transactions,它存储了一个个的Transaction类的实例,该类有三个属性:payee,amount,date。下面以此为例说明如何使用这些运算符:
要获取amount的平均值可以这样:
[java] view plaincopy
 
  1. NSNumber *transactionAverage = [transactions valueForKeyPath:@"@avg.amount"];  
要获取transactions集合中元素数目可以这样:
[java] view plaincopy
 
  1. NSNumber *numberOfTransactions = [transactions valueForKeyPath:@"@count"];  
需要之处的是,@count是这些集合运算符中比较特殊的一个,因为它没有右路经,原因很容易理解。
②对象运算符
比集合运算符稍微复杂,能以数组的方式返回指定的内容,一共有两种:
[java] view plaincopy
 
  1. @distinctUnionOfObjects  
  2. @unionOfObjects  
它们的返回值都是NSArray,区别是前者返回的元素都是唯一的,是去重以后的结果;后者返回的元素是全集。
用法如下:
[java] view plaincopy
 
  1. NSArray *payees = [transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];  
  2. NSArray *payees = [transactions valueForKeyPath:@"@unionOfObjects.payee"];  
前者会将收款人的姓名去除重复的以后返回,后者直接返回所有收款人的姓名。
③Array和Set操作符
这种情况更复杂了,说的是集合中包含集合的情况,我们执行了如下的一段代码:
[java] view plaincopy
 
  1. // Create the array that contains additional arrays.  
  2. self.arrayOfTransactionsArray = [NSMutableArray array];  
  3.    
  4. // Add the array of objects used in the above examples.  
  5. [arrayOfTransactionsArray addObject:transactions];  
  6.    
  7. // Add a second array of objects; this array contains alternate values.  
  8. [arrayOfTransactionsArrays addObject:moreTransactions];  
得到了一个包含集合的集合:arrayOfTransactionsArray
这时如果我们想操作arrayOfTransactionsArray中包含的集合中的元素时,可以使用如下三个运算符:
[java] view plaincopy
 
  1. @distinctUnionOfArrays  
  2. @unionOfArrays  
  3. @distinctUnionOfSets  
前两个针对的集合是Arrays,后一个针对的集合是Sets。因为Sets中的元素本身就是唯一的,所以没有对应的@unionOfSets运算符。
它们的用法举例如下:
[java] view plaincopy
 
  1. NSArray *payees = [arrayOfTransactionsArrays valueForKeyPath:@"@unionOfArrays.payee"];  

三、实现原理

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_Person类会重写以下方法:
增加了监听的属性对应的set方法,class,dealloc,_isKVOA。
①class
重写class方法是为了我们调用它的时候返回跟重写继承类之前同样的内容。
打印如下内容:
[java] view plaincopy
 
  1. NSLog(@"self->isa:%@",self->isa);  
  2. NSLog(@"self class:%@",[self class]);  
在建立KVO监听前,打印结果为:
[java] view plaincopy
 
  1. self->isa:Person  
  2. self class:Person  
在建立KVO监听之后,打印结果为:
[java] view plaincopy
 
  1. self->isa:NSKVONotifying_Person  
  2. self class:Person  
这也是isa指针和class方法的一个区别,大家使用的时候注意。
②重写set方法
新类会重写对应的set方法,是为了在set方法中增加另外两个方法的调用:
[java] view plaincopy
 
  1. - (void)willChangeValueForKey:(NSString *)key  
  2. - (void)didChangeValueForKey:(NSString *)key  
其中,didChangeValueForKey:方法负责调用:
[java] view plaincopy
 
  1. - (void)observeValueForKeyPath:(NSString *)keyPath  
  2.                       ofObject:(id)object  
  3.                         change:(NSDictionary *)change  
  4.                        context:(void *)context  
方法,这就是KVO实现的原理了!
如果没有任何的访问器方法,-setValue:forKey方法会直接调用:
[java] view plaincopy
 
  1. - (void)willChangeValueForKey:(NSString *)key  
  2. - (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不够完美,不能算是缺陷。
 
参考文档:
http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/KeyValueCoding/Articles/KeyValueCoding.html#//apple_ref/doc/uid/10000107-SW1
http://blog.csdn.net/kesalin/article/details/8194240

KVC/KVO原理详解及编程指南(转载)


推荐阅读
  • 本文讨论了Alink回归预测的不完善问题,指出目前主要针对Python做案例,对其他语言支持不足。同时介绍了pom.xml文件的基本结构和使用方法,以及Maven的相关知识。最后,对Alink回归预测的未来发展提出了期待。 ... [详细]
  • Mac OS 升级到11.2.2 Eclipse打不开了,报错Failed to create the Java Virtual Machine
    本文介绍了在Mac OS升级到11.2.2版本后,使用Eclipse打开时出现报错Failed to create the Java Virtual Machine的问题,并提供了解决方法。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 本文介绍了通过ABAP开发往外网发邮件的需求,并提供了配置和代码整理的资料。其中包括了配置SAP邮件服务器的步骤和ABAP写发送邮件代码的过程。通过RZ10配置参数和icm/server_port_1的设定,可以实现向Sap User和外部邮件发送邮件的功能。希望对需要的开发人员有帮助。摘要长度:184字。 ... [详细]
  • nil用来给对象赋值(Objective-C中的任何对象都属于id类型),NULL则给任何指针赋值,NULL和nil不能互换,nil用于类指针赋值(在Objective-C中类是一 ... [详细]
  • iOS安全攻防(二十四):敏感逻辑的保护方案(1)Objective-C代码容易被hook,暴露信息太赤裸裸,为了安全,改用C来写吧!当然不是全部代码都要C来写,我指的是敏感业务逻 ... [详细]
  • 2015年iOS测试现状
    本文由伯乐在线-nathanw翻译,dopcn校稿。未经许可,禁止转载!英文出处:www.mokacoding.com。欢迎加入翻译小组。几周前,我决定将将我在mokacoding ... [详细]
  • Monkey《大话移动——Android与iOS应用测试指南》的预购信息发布啦!
    Monkey《大话移动——Android与iOS应用测试指南》的预购信息已经发布,可以在京东和当当网进行预购。感谢几位大牛给出的书评,并呼吁大家的支持。明天京东的链接也将发布。 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • 高质量SQL书写的30条建议
    本文提供了30条关于优化SQL的建议,包括避免使用select *,使用具体字段,以及使用limit 1等。这些建议是基于实际开发经验总结出来的,旨在帮助读者优化SQL查询。 ... [详细]
  • 本文介绍了指针的概念以及在函数调用时使用指针作为参数的情况。指针存放的是变量的地址,通过指针可以修改指针所指的变量的值。然而,如果想要修改指针的指向,就需要使用指针的引用。文章还通过一个简单的示例代码解释了指针的引用的使用方法,并思考了在修改指针的指向后,取指针的输出结果。 ... [详细]
  • 在project.properties添加#Projecttarget.targetandroid-19android.library.reference.1..Sliding ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
author-avatar
郭伟健逍遥_308
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有