KVO底层原理和KVC

830 阅读4分钟

一、KVO底层原理

KVO的全称 Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。 是用kvo监听对象,对象的isa指针指向了一个中间以NSKVONotifying_开头的派生类,NSKVONotifying_这个类的superClass指向原来对象的类对象,也就是系统使用runtime动态生成了一个子类. NSKVONotifying_这个动态生成的类对象在内存中包含了属性的setter方法, dealloc方法, isKVOA方法, 以及class方法 重写class方法是为了屏蔽内部的实现,返回原来的类型 重写dealloc方法是为了做一些收尾的工作

屏幕快照 2021-08-07 下午3.33.31.png

我们以setAge:方法为例分析:

在添加kvo前后setAge:方法实现的地址发生了变化,添加kvo以后指向了c语言foundation框架的_NSsetIntValueAndNotify函数。实际上Foundation框架中会根据属性的类型,调用不同的方法。

图片1.png

_NSSet*ValueAndNotify的内部实现:

  • 调用willChangeValueForKey:
  • 调用原来类的setter方法([super setAge:age])
  • 调用didChangeValueForKey:
  • didChangeValueForKey:内部会调用observer的observerValueForKeyPath:ofObject:change:context:方法

1.怎么样手动触发KVO? 手动调用willChangeValueForKey:和didChangeValueForKey:即可以在不改变属性值得前提下,手动触发KVO,并且这两个方法缺一不可。

Person *p1 = [[Person alloc] init]; 
p1.age = 1.0; 
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; 
[p1 addObserver:self forKeyPath:@"age" options:options context:nil]; 
[p1 willChangeValueForKey:@"age"]; 
[p1 didChangeValueForKey:@"age"]; 
[p1 removeObserver:self forKeyPath:@"age"];

二、KVO是用过程中注意事项

被观察者在销毁前,要移除所有的观察者,iOS10以下会崩溃,iOS11以上不会崩溃

三、可能的崩溃原因以及解决方案

崩溃原因:

  1. observe忘记写回调方法observerValueForKeyPath
  2. add和remove次数不匹配
  3. 监听者和被监听者dealloc之前没有remove(原因其实和2相同,但是监听者和被监听者的生命周期不同)

解决方案:

  1. 给NSObject增加分类方法MM_addObserver:ForKeyPath:方法。
  2. 自定义对象observerHelper,声明属性target,observer,自定义对象dealloc中调用 target的 removeObserver:forKeyPath:。
  3. NSObject增加的分类方法MM_addObserver:ForKeyPath中,调用 addObserver:forKeyPath:,及创建对象observerHelper,给对象关联一个observerHelper对象属性。这样self释放的时候,就会释放helper,helper的dealloc中又会removeObserver:forKeyPath。

#import <Foundation/Foundation.h>
@interface NSObject (ObserverHelper)
-(void)cc_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end

#import "NSObject+ObserverHelper.h"
#import <objc/message.h>
@interface ObserverHelper : NSObject
@property (nonatomic, unsafe_unretained) id target;
@property (nonatomic, unsafe_unretained) id observer;
@property (nonatomic, strong) NSString *keyPath;
@property (nonatomic, weak) ObserverHelper *factor;
@end

@implementation ObserverHelper
-(void)dealloc {
if ( _factor ) {
[_target removeObserver:_observer forKeyPath:_keyPath];
}
}

@end

@implementation NSObject (ObserverHelper)
-(void)cc_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {

    [self addObserver:observer forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:nil];
ObserverHelper *helper = [ObserverHelper new];
ObserverHelper *sub = [ObserverHelper new];
sub.target = helper.target = self;
sub.observer = helper.observer = observer;
sub.keyPath = helper.keyPath = keyPath;
helper.factor = sub;
sub.factor = helper;
const char *helpeKey = [[keyPath mutableCopy] UTF8String];
const char *subKey = [[keyPath mutableCopy] UTF8String];
// 关联属性  举例 self 和 helper 关联 当self释放的时候 helper释放 即可释放self的kvo 观察者和sub关联 当观察者释放的时候 调用sub的移除同样也能删除self的kvo   factor是同一个对象 是为防止多次移除导致的崩溃
objc_setAssociatedObject(self, helpeKey, helper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    objc_setAssociatedObject(observer, subKey, sub, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

一、KVC

KVC的全称是Key-Value Coding, 俗称“键值编码”, 可以通过一个key来访问某个属性。 常见的API有:

-(voidsetValue:(id)value forKey:(NSString *)key;
-(voidvalueForKey:(NSString *)key;
-(voidsetValue:(id)value forKeyPath:(NSString *)keyPath;
-(voidvalueForKeyPath:(NSString *)keyPath;

kvc中setValue:forKey:和setValue:forKeyPath:的区别

[person setValue:@10 forKey:@"age"];
//person对象里面有一个cat对象, 可以通过forKeyPath来直接修改cat变量的属性值
[person setValue:@10 forKeyPath:@"cat.weight"];

直接修改成员变量的值,不会触发kvo监听,因为kvo的底层原理是通过set方法来调用的
通过KVC修改属性的值或者成员变量的值是可以触发KVO的。原因是什么呢?
我们来看一下KVC的底层是怎么实现的:
setValue:forKey的原理:

屏幕快照 2021-08-08 下午10.43.50.png

valueForKey:的原理:

屏幕快照 2021-08-08 下午10.44.06.png

在KVC赋值的过程中,底层会顺序查找setKey:或者_setKey:的方法,找到之后传递参数调用方法,修改实例对象的值,直接调用相当于直接调用set方法,符合KVO底层的调用原理。修改成员的变量的值得时候,因为没有找到set方法,所以会顺序查找_key, _isKey, key, isKey成员变量,然后修改值得过程中,调用了willChangeValueForKey和didChangeValueForKey方法,也就是相当于手动触发了KVO。