KVO底层原理
- KVO概述
- KVO常用方法
- 注册监听器
- 详细解释
- 1. 系统不会增加观察者对象的引用计数
- 2. 对象释放后观察者不会自动置空
- 3. 需要自己持有观察者对象的强引用
 
- 示例代码
- Person 类
- Observer 类
- main 函数
 
- 解释
- 删除监听器
- 监听器对象的监听回掉方法
 
- KVO内部实现
- _NSSetLongLongValueAndNotify
 
 
KVO概述
KVO是为了监听一个对象的某个属性值是否发生变化。在属性值发生变化的时候,肯定会调用其setter方法。所以KVO的本质就是监听 被监听对象有没有调用被监听属性对应的setter方法。
KVO常用方法
-(void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
注册监听器
- 监听器对象为observer,被监听对象为消息的发送者即方法的调用者在回调函数中会被回传
- 监听的属性路径为keyPath支持点语法的嵌套
- 监听类型为options支持按位或来监听多个事件类型
- 监听上下文context主要用于在多个监听器对象监听相同keyPath时进行区分
- 添加监听器只会保留监听器对象的地址,不会增加引用,也不会在对象释放后置空,因此需要自己持有监听对象的强引用,该参数也会在回调函数中回传
对于第5条,防崩溃,正确使用方式如下:
这句话解释了KVO机制中的一个关键点:当你向一个对象添加观察者时,系统不会增加对观察者对象的引用计数,这意味着观察者对象可能会在被观察对象存活期间被释放。因此,你需要自己持有观察者对象的强引用,以确保观察者在观察期间不会被释放。
详细解释
1. 系统不会增加观察者对象的引用计数
当你调用addObserver:forKeyPath:options:context:方法时,系统只会记录观察者对象的地址,但不会增加它的引用计数。这意味着系统不会自动管理观察者对象的内存。
2. 对象释放后观察者不会自动置空
如果观察者对象被释放,而没有在被观察对象中移除观察者,那么当被观察对象的属性发生变化时,系统仍然会尝试通知已经被释放的观察者,导致崩溃。因此,在观察者对象被释放之前,你需要确保将它从被观察对象的观察列表中移除。
3. 需要自己持有观察者对象的强引用
为了避免观察者对象在观察期间被释放,你需要在合适的位置持有观察者对象的强引用。通常可以在控制器或管理类中将观察者对象作为一个属性来持有。
示例代码
下面是一个示例,展示如何正确持有观察者对象的强引用:
Person 类
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation Person
@end
Observer 类
@interface Observer : NSObject
@property (nonatomic, strong) Person *person;
@end
@implementation Observer
- (instancetype)init {
    self = [super init];
    if (self) {
        _person = [[Person alloc] init];
        [_person addObserver:self
                  forKeyPath:@"name"
                     options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                     context:nil];
    }
    return self;
}
- (void)dealloc {
    [_person removeObserver:self forKeyPath:@"name"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"Name changed from %@ to %@",
              change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
@end
main 函数
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Observer *observer = [[Observer alloc] init];
        observer.person.name = @"John"; // 触发KVO
        observer.person.name = @"Doe";  // 触发KVO
    }
    return 0;
}
解释
-  在Observer类中持有Person对象的强引用: @property (nonatomic, strong) Person *person;这样可以确保 Person对象在Observer对象的生命周期内不会被释放。
-  在Observer类的init方法中添加观察者: [_person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
-  在Observer类的dealloc方法中移除观察者: [_person removeObserver:self forKeyPath:@"name"];
删除监听器
- 监听器对象为observer,被监听对象为消息的发送者即方法的调用者,应与addObserver方法匹配
- 监听的属性路径为keyPath,应与addObserver方法的keyPath匹配
- 监听上下文context,应与addObserver方法的context匹配
-(void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
监听器对象的监听回掉方法
- keyPath即为监听的属性路径
- object为被监听的对象
- change保存被监听的值产生的变化
- context为监听上下文,由add方法回传
 -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
 change的值:
- NSKeyValueChangeNewKey - 类型值为id,表示新的属性值
- NSKeyValueChangeOldKey - 类型值为id,表示旧的属性值
- NSKeyValueChangeKindKey - 表示属性变化的类型,类型为NSNumber (包含NSKeyValueChange枚举值) 
  - NSKeyValueChangeSetting(设置新的值)
- NSKeyValueChangeInsertion(插入元素,通常用于集合)
- NSKeyValueChangeRemoval(移除元素,通常用于集合)
- NSKeyValueChangeReplacement(替换元素,通常用于集合)
 
- NSKeyValueChangeIndexesKey 
  - 类型为NSIndexSet
- 表示插入、移除或替换操作的索引。通常用于集合类型的属性,如数组。
 
- NSKeyValueChangeNotificationIsPriorKey 
  - 类型为NSNumber
- 如果存在此键且值为YES,则表示这是一个更改前的通知。
 
综合代码示例:
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        id newValue = change[NSKeyValueChangeNewKey];
        id oldValue = change[NSKeyValueChangeOldKey];
        NSNumber *kind = change[NSKeyValueChangeKindKey];
        NSIndexSet *indexes = change[NSKeyValueChangeIndexesKey];
        NSNumber *isPrior = change[NSKeyValueChangeNotificationIsPriorKey];
        NSLog(@"Name changed from %@ to %@", oldValue, newValue);
        switch (kind.integerValue) {
            case NSKeyValueChangeSetting:
                NSLog(@"Setting new value");
                break;
            case NSKeyValueChangeInsertion:
                NSLog(@"Inserting new element");
                break;
            case NSKeyValueChangeRemoval:
                NSLog(@"Removing element");
                break;
            case NSKeyValueChangeReplacement:
                NSLog(@"Replacing element");
                break;
        }
        if (indexes) {
            NSLog(@"Changed indexes: %@", indexes);
        }
        if (isPrior.boolValue) {
            NSLog(@"This is a prior notification");
        }
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
KVO内部实现
未使用KVO监听的对象:
 
使用了KVO监听的对象:
 
- 重写class方法是为了我们调用它的时候返回跟重写继承类之前同样的内容。KVO底层交换了 NSKVONotifying_Person 的 class 方法,让其返回 Person
- 重写setter方法:在新的类中会重写对应的set方法,是为了在set方法中增加另外两个方法的调用
- (void)willChangeValueForKey:(NSString *)key
(void)didChangeValueForKey:(NSString *)key
在didChangeValueForKey:方法再调用以下方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
- 重写dealloc方法,销毁新生成的NSKVONotifying_类。
- 重写_isKVOA方法,这个私有方法估计可能是用来标示该类是一个 KVO 机制声称的类。
_NSSetLongLongValueAndNotify
在添加KVO监听方法以后setAge方法变成了_NSSetLongLongValueAndNotify,所以我们可以大概猜测动态监听方法主要就是在这里面实现的
我们可以在终端使用nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation | grep ValueAndNotify命令来查看NSSet*ValueAndNotify的类型
 
我们可以在Person类中重写willChangeValueForKey和didChangeValueForKey,来猜测一下_NSSetLongLongValueAndNotify的内部实现
- (void)setAge:(NSInteger)age{
	_age = age;
	NSLog(@"调用set方法");
}
- (void)willChangeValueForKey:(NSString *)key{
	[super willChangeValueForKey:key];
	NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key{
	NSLog(@"didChangeValueForKey - begin");
	[super didChangeValueForKey:key];
	NSLog(@"didChangeValueForKey - end");
}
打印结果:
 
由此得出 _NSSetLongLongValueAndNotify内部实现为:
- 调用willChangeValueForKey方法
- 调用setAge方法
- 调用didChangeValueForKey方法
- didChangeValueForKey方法内部调用observer的observeValueForKeyPath:ofObject:change:context:方法



















