KVO探究

来源:互联网 发布:网络节点怎么设置 编辑:程序博客网 时间:2024/05/01 05:41

 KVO探究

NSKeyValueCoding

这种统一的直接通过字符串存取ObjC中对象的成员属性的接口,可以实现由外部脚本控件程序执行或者获取程序执行信息。通过KVC存取二进制库中的私有成员也比较实用。普通开发的确不会并且不需要用太多。
KVC处理业务很少用到, 但是却是KVO和CoreData的基础,

  1. 基本概念
    MODEL
    主要是英文文档里面经常出现的一些概念,讲解一下,方便英文文档的阅读。
    IOS应用开发是遵循MVC设计模式的,Cocoa框架用Object Modeling的规则来规范一个Model的实现。
    ObjectModeling有如下几个概念的规定:
    Entity:表示持有数据的一个实体
    Property实体中的成员,分为Attribute和:Relationship
    Attribute:基本类型的成员,比如:数字、NSString。
    Relationship:指向其它Entity的关系型成员,它又有to 1Relationship和to manyRelationship的区别。
    AccessorMethod:getter,setter。
    举例:
    如下是一个部门和员工关系的Model


使用KVC、KVO的优势
通过规定了一组通用的Cocoa命名法则、调用规则等,实现了如下功能:
² 使用一对高度规范化的访问方法,获取以及设置任何对象的任何属性的值。
² 通过继承一个特定的方法,并且指定希望监视的对象及希望监视的属性名称,就能在该对象的指定属性的值发生改变时,得到一个“通知”(尽管这不是一个真正意 义上的通知),并且得到相关属性的值的变化(原先的值和改变后的新值)。
² 通过一个简单的函数调用,使一个视图对象的一个指定属性随时随地都和一个控制器对象或模型对象的一个指定属性保持同步。

2. KVC
2.1 概述

KVC是KeyValue Coding的简称,它是一种可以直接通过字符串的名字(key)来访问类属性的机制。而不是通过调用Setter、Getter方法访问。
当使用KVO、Core Data、CocoaBindings、AppleScript(Mac支持)时,KVC是关键技术。

2.2 如何使用KVC
关键方法定义在:NSKeyValueCodingprotocol
KVC支持类对象和内建基本数据类型。

2.2.1 获取值
valueForKey:,传入NSString属性的名字。
valueForKeyPath:,传入NSString属性的路径,xx.xx形式。
valueForUndefinedKey它的默认实现是抛出异常,可以重写这个函数做错误处理。
2.2.2 修改值
setValue:forKey:
setValue:forKeyPath:
setValue:forUndefinedKey:
setNilValueForKey: 当对非类对象属性设置nil时,调用,默认抛出异常。
2.2.3 一对多关系成员的情况
mutableArrayValueForKey:有序一对多关系成员 NSArray
mutableSetValueForKey:无序一对多关系成员 NSSet

2.3 KVC的实现细节

搜索Setter、Getter方法
 这一部分比较重要,能让你了解到KVC调用之后,到底是怎样获取和设置类成员值的。
2.3.1 搜索简单的成员
如:基本类型成员,单个对象类型成员:NSInteger,NSString成员。
*
a. setValue:forKey的搜索方式:**

  1. 首先搜索set<Key>:方法
    如果成员用@property,@synthsize处理,因为@synthsize告诉编译器自动生成set<Key>:格式的setter方法,所以这种情况下会直接搜索到。

  2. 上面的setter方法没有找到,如果类方法accessInstanceVariablesDirectly返回YES(注:这是NSKeyValueCodingCatogery中实现的类方法,默认实现为返回YES)。
    那么按_<key>,_is<Key>,<key>,is<key>的顺序搜索成员名。

  3. 如果找到设置成员的值,如果没有调用setValue:forUndefinedKey:。

b. valueForKey:的搜索方式:

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

2.3.2 查找有序集合成员,比如NSMutableArray
mutableArrayValueForKey:搜索方式如下:

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

2.3.3 搜索无序集合成员,如:NSSet。
mutableSetValueForKey:搜索方式如下:

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

KVC还提供了下面的功能
2.4 值的正确性核查

KVC提供属性值确认的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
实现核查方法
为如下格式:validate<Key>:error:
如:

-(BOOL)validateName:(id *)ioValue error:(NSError **)outError{    // The name must not be nil, and must be at least two characters long.    if ((*ioValue == nil) || ([(NSString *)*ioValue length] < 2]) {        if (outError != NULL) {            NSString *errorString = NSLocalizedStringFromTable(                    @"A Person's name must be at least two characters long", @"Person",                    @"validation: too short name error");            NSDictionary *userInfoDict =                [NSDictionary dictionaryWithObject:errorString                                            forKey:NSLocalizedDescriptionKey];            *outError = [[[NSError alloc] initWithDomain:PERSON_ERROR_DOMAIN                                                    code:PERSON_INVALID_NAME_CODE                                                userInfo:userInfoDict] autorelease];        }        return NO;    }    return YES;}

调用核查方法:
validateValue:forKey:error:,默认实现会搜索 validate<Key>:error:格式的核查方法,找到则调用,未找到默认返回YES。
注意其中的内存管理问题。

2.5 集合操作
集合操作通过对valueForKeyPath:传递参数来使用,一定要用在集合(如:array)上,否则产生运行时刻错误。其格式如下:

Left keypath部分:需要操作对象路径。
Collectionoperator部分:通过@符号确定使用的集合操作。
Rightkey path部分:需要进行集合操作的属性。
2.5.1 数据操作
@avg:平均值
@count:总数
@max:最大
@min:最小
@sum:总数
确保操作的属性为数字类型,否则运行时刻错误。
2.5.2 对象操作
针对数组的情况
@distinctUnionOfObjects:返回指定属性去重后的值的数组
@unionOfObjects:返回指定属性的值的数组,不去重
属性的值不能为空,否则产生异常。
2.5.3 数组操作
针对数组的数组情况
@distinctUnionOfArrays:返回指定属性去重后的值的数组
@unionOfArrays:返回指定属性的值的数组,不去重
@distinctUnionOfSets:同上,只是返回值为NSSet

2.6 效率问题
相比直接访问KVC的效率会稍低一点,所以只有当你非常需要它提供的可扩展性时才使用它。

2.7 Key Value Coding 

Key Value Coding是cocoa的一个标准组成部分,它能让我们可以通过name(key)的方式访问property, 不必调用明确的property accssor, 如我们有个property叫做foo, 我们可以foo直接访问它,同样我们也可以用KVC来完成[Object valueForKey:@“foo”], 有同学就会问了, 这样做有什么好处呢?主要的好处就是来减少我们的代码量。

下面我们来看看几个例子,就明白了KVO的用法和好处了,假设这样个类叫做People

@interface People: NSObject @property (nonatomic, strong) NSString *name; @property (nonatomic, strong) NSNumber *age; @end

场景1,

这下我们有了server, server的某个api(listPeople??), 会返回我们json格式一个数组,里面包含这样dict{name:xx, age:xx}这样的数据, 我们希望用这些dict数据构造出我们的people来,通常我们的做法是,为我们People类写一个static factory方法专门用来处理dict来, 把dict里面的数据取出来, 然后创建个空的People对象,然后依次设置property。然而当这样类似People的与server交互的类多了,我们就要为每个类都要加上这样的wrapper, 是否有种简单办法来设置这样的属性,当然就是我们的KVC了。

- (id)initWithDictionary:(NSMutableDictionary*)jsonObject {     if((self = [super init]))     {         [self init];         [self setValuesForKeysWithDictionary:jsonObject];     }     return self; }

setValuesForKeysWithDictionary, 会为我们把和dictionary的key名字相同的class proerty设置上dict中key对应的value, 是不是很方便呀,但是有同学又要问了 如果json里面的某些key就是和object的property名字不一样呢,或者有些server返回的字段是objc保留字如”id”, “description”等, 我们也希望也map dict to object, 这时候我们就需要用上setValue:forUndefinedKey, 因为如果我们不处理这些Undefined Key,还是用setValuesForKeysWithDictionary就会 抛出异常。

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {     if([key isEqualToString:@"nameXXX"])         self.name = value;     if([key isEqualToString:@"ageXXX"])         self.age = value;     else         [super setValue:value forKey:key]; }

所以只要重载这个方法,就可以处理了那些无法跟property相匹配的key了,默认的实现是抛出一个NSUndefinedKeyException,又有同学发问了如果 这时候server返回的People有了内嵌的json(如Products{product1{count:xx, sumPrice:xx}}, product2{} ….),又该怎么办,能把这个内嵌的json转化成我们的客户端的Product类嘛, 当然可以这时候就需要重载setValue:forKey, 单独处理”Products”这个key, 把它wrapper成我们需要的class

-(void) setValue:(id)value forKey:(NSString *)key {   if([key isEqualToString:@"products"])   {     for(NSMutableDictionary *productDict in value)     {       Product *product = [[Product alloc] initWithDictionary:productDict];       [self.productList addObject:product];     }   }   else  {    [super setValue:value forKey:key] ;  }

场景2,

我们需要把一个数组里的People的名字的首字母大写,并且把新的名字存入新的数组, 这时候通常做法会是遍历整个数组,然后把每个People的name取出来,调用 capitalizedString 然后把新的String加入新的数组中。 有了KVC就有了新做法:

[array valueForKeyPath:@"name.capitalizedString"]

我们看到valueForKeyPath, 为什么用valueForKeyPath, 不用valueForKey, 因为valueForKeyPath可以传递关系,例如这里是每个People的name property的String的capitalizedString property, 而valueForKey不能传递这样的关系,所以对于dict里面的dict, 我们也只能用valueForKeyPath。这里我们也看到KVC对于array(set), 做了特殊处理,不是简单操作collection上,而是 针对这些collection里面的元素进行操作,同样KVC也提供更多地操作,例如@sum这些针对collection,有兴趣的同学可以去用下。

场景3,

当我们执行NSArray *products = [people valueForKey:@“products”],我们希望的是[people products],可是people没有这样的方法, KVC又会为我们带来些什么呢?
首先会去找getProdcuts or products or isProducts, 按照这样的顺序去查找,第一个找到的就返回
然后会去找countOfProducts and either objectInProductsAtIndex: or ProductsAtIndexes,
如果找到,就会去找countOfProducts and enumeratorOfProducts and memberOfProducts 这个2个方法都找到了,KVC才会给我们返回一个代理的NSKeyValueArray,用于我们后续的操作(addProduct之类的)。

如果有个变量叫做 products, isProducts, products or isProducts, KVC会直接就使用这样的变量,如果你觉得直接用这样的变量是破坏了封装, 可以禁止这样的行为发生,重载 +accessInstanceVariablesDirectly,返回NO。

简单来说,valueForKey, 会给我们带来一个代理array, 如果我们实现了某些方法,上诉的这些方法只是针对NSArray, 对于mutable的collection, 我们还需要提供其他 方法的实现才行。

2.8 KVO精华总结

这种统一的直接通过字符串存取ObjC中对象的成员属性的接口,可以实现由外部脚本控件程序执行或者获取程序执行信息。
通过KVC存取二进制库中的私有成员也比较实用。
也是使用KVO和CoreData的基础。   
先看这个Class

@interface Teacher : NSObject{    @private    NSNumber *age ;  }@property (nonatomic,copy,readonly)     NSString   *name ;@property (nonatomic,strong)            Student    *student ;- (instancetype)initWithStudent:(Student *)student ;- (void)logAge ;@end看到这个类有私有变量,和只读变量, 如果用一般的settergetter, 在类外部是者访问到私有变量的, 不能重写只读变量,那是不是就拿它没办法了呢?

然而KVC就是这么神奇

1.修改只读readonly变量
readonly只读. 是不能直接赋值的, 但是KVC可以 .
e.g. 
如果想这样teacher1.name = @"张三" 是不行的,因为name是只读变量. 可以通过KVC

    [teacher1 setValue:@"Teacher_teason" forKey:@"name"] ;

2.修改私有private变量
KVC可以修改一个对象的属性和变量, 即使它是私有的.
e.g.
teacher1.age = 24 ;肯定不能用  

[teacher1 setValue:@24 forKey:@"age"] ;

如果变量名字为"_age"那么用age和_age`都可以, KVC内部逻辑是先查找age在查找_age

3.通过运算符层次查找对象的属性

    Student *student1 = [[Student alloc] initWithName:@"Student_Teason" bookList:mutableList] ;    Teacher *teacher1 = [[Teacher alloc] initWithStudent:student1] ;
    NSLog(@"All book name  : %@",[teacher1 valueForKeyPath:@"student.bookList.name"]) ;    NSLog(@"All book name  : %@",[student1 valueForKeyPath:@"bookList.name"]) ;

这两个打印的结果是一样的 .通过keyPath直接访问属性

4.获取数组

    for (Book *book in [student1 valueForKey:@"bookList"]) {     // [student1 valueForKey:@"bookList"]返回一个数组        NSLog(@"bookName : %@ \t price : %f",book.name,book.price) ;    }

5.对属性进行数学运算

NSLog(@"sum of book price : %@",[student1 valueForKeyPath:@"bookList.@sum.price"]) ;    NSLog(@"avg of book price : %@",[student1 valueForKeyPath:@"bookList.@avg.price"]) ;

0 0