iOS KVC和KVO

kvc:使用字符串直接访问对象的属性,或者给对象属性赋值

kvo:键值观察机制,它提供了观察对象属性变化的方法

KVC的底层实现

当一个对象调用setValue方法时,方法内部会做以下操作:

1.检查是否存在相应key的set方法,如果存在,就调用set方法

2.如果set方法不存在,就会查找与key相同名称并且带下划线的成员属性,如果有,则直接给成员属性赋值

3.如果没有找到_key,就会查找相同名称的属性key,比如_iskey,iskey,如果有就直接赋值

4.如果还没找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法

如果开发者想让这个类禁用KVC,那么重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO即可,这样的话如果KVC没有找到set:属性名时,会直接用setValue:forUndefinedKey:方法。

当一个对象调用valueForKey方法时,方法内部会做以下操作:

1.首先按get,,is的顺序方法 查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。

2.如果上面的getter没有找到,KVC则会查找countOf,objectInAtIndex或AtIndexes格式的方法。如果countOf方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf,objectInAtIndex或AtIndexes这几个方法组合的形式调用。还有一个可选的get:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。

3.如果上面的方法没有找到,那么会同时查找countOf,enumeratorOf,memberOf格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给 这个代理集合发NSSet的消息,就会以countOf,enumeratorOf,memberOf组合的形式调用

4.如果还没有找到,再检查类方法+
(BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_,_is,,is的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会
直接调用valueForUndefinedKey:方法,默认是抛出异常

KVO的底层实现

kvo基于runtime机制实现

使用了isa混写(isa-swizzling),当一个对象(假设是person对象,person的类是MyPerson)的属性值(假设person的age)被观察时(addObserver:),系统会自动生成一个类,继承自MyPerson,NSKVONotifying_MyPerson,打印这个子类可以看到内部也有一个 setName:方法 还重写了 class 和 dealloc 方法 , _isKVOA

官方解释:当某个类的对象一次被观察时,系统就会在运行时动态的创建该类的一个派生类(子类)

在这个类的setAge方法里面,调用

[self willChangeValueForKey:@"age"] 
[self setAge:age] 
[self didChangeValueForKey:@"age"]

而这两个方法内部会主动调用监听者内部的- (void)observeValueForKeyPath 这个方法。

KVO触发模式

KVO在属性发生改变时的调用是自动的,如果想要手动控制这个调用时机,或想自已实现KVO属性的调用,则可以通过KVO提供的方法进行调用。
默认是点语法调用setter方法触发,如果手动,得代码控制:

@implementation Person
/** 模式调整 */
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    return NO;  // 改为手动模式
}
@end

这样在ViewController中改name的值不会进到监听方法中,需要手动调用触发,在更改name地方需要做如下处理:

/** 屏幕touch */
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    static int magicNum;
    [p1 willChangeValueForKey:NSStringFromSelector(@selector(name))];
    p1.name = [NSString stringWithFormat:@"name=%d", magicNum++];
    [p1 didChangeValueForKey:NSStringFromSelector(@selector(name))];
}

手动模式的好处,可能有一种需求在某些情况下在更改value的时候,不需要通知,有的时候需要通知,这个时候就需要手动模式来处理。
如果把上面代码中对Person中的name赋值给注视掉,再次去点击屏幕,会发现还是会进到监听方法中,这种情况下,监听方法调不调用与设置name无关,只是和有没有调用方法willChangeValueForKey:和didChangeValueForKey:有关。

KVO容器观察

如果Person中有个容器属性,这需要怎么观察到容器中数据改动。

@property (nonatomic, strong) NSMutableArray* arrayValue;

同样通过上述注册方法对arrayValue进行观察,然后每次点击屏幕的时候都给arrayValue添加一个元素,如下:

[p1.arrayValue addObject:@"1"];

会发现回调方法不会触发,这个由于KVO观察的是set方法,这边容器是add,所以就不会触发,KVO给开发者提供了mutableArrayValueForKey去拿容器对象,然后再调用add,这个时候就会观察到元素改变:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //tempArray : (_NSStackBlock_*)
    NSMutableArray* tmpArray = [p1 mutableArrayValueForKey:@"arrayValue"];
    //tempArray : (NSKeyValueNotifyingMutableArray *)
    [tmpArray addObject:@"1"];
}

tempArray 从 (NSStackBlock*) 变成了 (NSKeyValueNotifyingMutableArray *),
tmpArray类型改变了,很明显是个子类,所以应该是系统在子类中重写了add方法,然后调用willChangeValueForKey:和didChangeValueForKey:两个方法通知外部达到目的。

KVO的优缺点

优点:

1.能够提供一种简单的方法实现两个对象间的同步

2.能够对非我们创建的对象,即内部对象的状态改变做出响应,而且不需要改变内部对象的实现

3.能够提供观察的属性的最新值以及先前值

4.用key paths来观察属性,因此也可以观察嵌套对象

5.完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察

缺点:

1.我们观察的属性必须使用string来定义,因此在编译期不会出现警告以及检查

2.对属性重构将导致我们的观察代码不再可用

3.复杂的if 语句要求对象正在观察多个值,这是因为所有的观察代码通过一个方法来指向

4.当释放观察者时需要移除观察者

你可能感兴趣的:(iOS KVC和KVO)