KVO

5/20/2021

原理:运行时创建子类,并重写监听属性的setter方法,在修改前后分别调用willChangeValueForKeydidChangeValueForKey方法,然后进行isa swizling。

# 实现原理

KVO是基于runtime机制实现的
当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制
如果原类为ClassName,那么生成的派生类名为NSKVONotifying_ClassName
每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法
键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey:didChangevlueForKey:;在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context:也会被调用。
补充: KVO的这套实现机制中苹果还偷偷重写了class方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类

# 如何手动关闭KVO

+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"closeType"]) {
        return NO;
    } else {
        return [super automaticallyNotifiesObserversForKey:key];
    }
}



-(void)setProperty:(NSString *)Property {
    if (_Property!=Property) {
        [self willChangeValueForKey:@"Property"];
        _Property=Property;
        [self didChangeValueForKey:@"Property"];
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 通过KVC修改属性会触发KVO吗

会触发KVO操作,KVC时候会先查询对应的 getter 和 setter 方法。
如果返回 YES,那么可以直接修改实例变量。

+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}
1
2
3
  • KVC 调用 getter 流程:getKEY,KEY,isKEY, _KEY,接着是实例变量 _KEY,_isKEY, KEY, isKEY;
  • KVC 调用 setter 流程:setKEY和 _setKEY,实例变量顺序 _KEY,_isKEY, KEY, isKEY,没找到就调用 setValue: forUndefinedKey:

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

  • dealloc没有移除kvo观察者
    解决方案:创建一个中间对象,将其作为某个属性的观察者,然后dealloc的时候去做移除观察者,而调用者是持有中间对象的,调用者释放了,中间对象也释放了,dealloc 也就移除观察者了;
  • 多次重复移除同一个属性,移除了未注册的观察者
  • 被观察者提前被释放,被观察者在 dealloc 时仍然注册着 KVO,导致崩溃。
    例如:被观察者是局部变量的情况(iOS 10 及之前会崩溃) 比如 weak ;
  • 添加了观察者,但未实现 observeValueForKeyPath:ofObject:change:context:方法,导致崩溃;
  • 添加或者移除时 keypath == nil,导致崩溃;

# KVO的优缺点

KVO优点:

  • 能够提供一种简单的方法实现两个对象的同步;
  • 能够对内部对象的状态改变作出响应,而且不需要改变内部对象的实现;
  • 能够提供被观察者属性的最新值和之前的值;
  • 使用key Path来观察属性,因此可以观察嵌套对象;
  • 完成了对观察对象的抽象,因为不需要额外的代码来允许观察者被观察。

KVO缺点:

  • KVO只能检测类中的属性,并且属性名都是通过NSString来查找,编译器不会补全(编译时不会出现警告),容易写错;
  • 对属性重构,将导致观察代码不可用;
  • 复杂的 “if” 语句要求对象正在观察多个值,是因为所有的观察代码通过一个方法来指向;
Last Updated: 1/15/2023, 2:48:14 PM