KVO面试题

263 阅读5分钟

1.KVO实现原理

基本的原理:

当观察某对象 A 时,KVO 机制就会动态创建一个对象A子类,并为这个新的子类重写被观察属性 ``keyPathsetter 方法。setter 方法随后负责通知观察对象属性的改变状况。

深入剖析

Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A 的新类,该类继承自对象A的本类,且 KVONSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原setter方法之前之后,通知所有观察对象属性值的更改情况。
(备注: isa 混写(isa-swizzlingisais a kind ofswizzling:混合,搅合;)

NSKVONotifying_A 类剖析:在这个过程,被观察对象的 isa 指针从指向原来的 A 类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_A 类,来实现当前类属性值改变的监听;
所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类,就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_A中间类,并指向这个中间类了。
isa 指针的作用:每个对象都有 isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa 指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。 )
因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。

我猜,这也是 KVO 回调机制,为什么都俗称 KVO 技术为黑魔法的原因之一吧:内部神秘、外观简洁。

子类 setter 方法剖析:KVO 的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:didChangevlueForKey:,在存取数值的前后分别调用 2 个方法:
被观察属性发生改变之前willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;
之后, observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的 setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:

- (void)setName:(NSString *)name{
    _NSSetObjectValueAndNotify();
}

void _NSSetObjectValueAndNotify {
    [self willChangeValueForKey:@"name"];//KVO 在调用存取方法之前总调用
    [super setName:name];//调用父类的存取方法
    [self didChangeValueForKey:@"name"];//KVO 在调用存取方法之后总调用
}

- (void)didChangeValueForKey:(NSString *)key{
    [observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}` 

面试答案

  1. KVO 基于 runtime 机制实现
  2. 使用了 isa 混写(isa-swizzling),当一个 person 对象的属性 age 发生改变时,系统会自动生成一个类,继承自 Person,在这个类的 setAge 方法里面,调用[super setAge:age][self willChangeValueForKey:@“age"][self didChangeValueForKey:@“age”],而这些方法内部会主动调用监听者内部的observeValueForKeyPath这个方法

2.如何手动关闭KVO?

被观察的对象复写如下方法 返回NO即可关闭KVO

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}` 

**如果关闭后还想触发 KVO 的话 ** 修改需要手动调用在变量setter的前后主动调用 willChangeValueForKey:didChangeValueForKey:

3.通过 KVC 修改属性会触发 KVO 吗?直接修改成员变量呢?

会触发,KVC 内部在修改成员变量的同时是会主动调用 didChangeValueForKeywillChangeValueForKey 去触发 KVO

直接修改成员变量不会触发 KVO,因为直接修改成员变量内部并没有处理,只是单纯的赋值

4.哪些情况下使用 KVO 会崩溃,怎么防护崩溃?

添加和移出不是成对出现且存在多线程添加KVO的情况
经常遇到的crash移出内存dealloc的时候,或者对象销毁前没有正确移出Observer

如何防护?

  1. 注意移出对象匹配
  2. 内存野指针问题,一定要在对象销毁前移出观察者
  3. 可以使用第三方库 BlockKit 添加 KVO , BlockKit 内部会自动移除 Observer 避免 crash.

5.KVO的优缺点

优点:

  1. 能够提供一种简单的方法实现两个对象间的同步。例如:modelview 之间同步;
  2. 能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象(SKD对象)的实现;
  3. 能够提供观察的属性的最新值以及先前值;
  4. key paths来观察属性,因此也可以观察嵌套对象;
  5. 完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察

缺点:

  1. 我们观察的属性必须使用 strings 来定义。因此在编译器不会出现警告以及检查;
  2. 对属性重构将导致我们的观察代码不再可用;
  3. 复杂的“IF”语句要求对象正在观察多个值,这是因为所有的观察代码通过一个方法来指向;
  4. 当释放观察者时需要移除观察者。

6.KVC原理

一、setValue:forKey:的原理

通过 KVC 赋值时, KVC 内部会通过传进去的 key 去寻找对应的 setter 方法. 按照顺序查找 setKey:、_setKey , 如果找到了方法, 就传递参数, 调用方法.

如果没有找到方法, 就会来到 accessInstanceVariablesDirectly 方法查看它的返回值, 如果返回 NO, 就调用 setValue:forUndefineKey: 并抛出异常 NSUnknownKeyException 崩溃. 如果返回 YES, 就会按照 _key_isKeykeyisKey 的顺序查找成员变量, 找到成员变量后直接赋值, 如果没有找到, 会也会调用 setValue:forUndefineKey: 并抛出异常 NSUnknownKeyException 崩溃.

accessInstanceVariablesDirectly 方法默认返回 YES. 可以用下面这幅图来表示: image.png

二、valueForKey:的原理

valueForKey 的原理跟 setValue:forKey: 的原理很像, 就不再赘述了, 直接上图: image.png

7.KVO 的使用场景:

  1. 实现上下拉刷新控件 content offset
  2. webView 混合排版 content size
  3. 监听模型属性实时更新UI