什么是KVO?
KVO是Key-Value Observing的简写,称为键值观察,用来监听对象属性的变化。
KVO使用探究
我们以IFPerson类为例研究KVO的使用。
自动触发KVO监听
- 主要代码
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person.name = @"kvo";
NSLog(@"class = %@",object_getClass(self.person));
//对person的name添加kvo监听
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
NSLog(@"class = %@",object_getClass(self.person));
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = [NSString stringWithFormat:@"%@ +",self.person.name];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@" === %@",change);
}
}
-(void)dealloc {
//移除监听
[self.person removeObserver:self forKeyPath:@"name"];
}
运行代码点击屏幕效果如下
手动触发KVO监听
如果想要手动触发KVO监听,需要重写+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key函数,该函数默认返回YES,表示自动触发,返回NO,则表示手动触发。对于本文,我们判断下key == name则手动触发,否则自动触发。修改代码如下
//在`IFPerson.m`中
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
-(void)setName:(NSString *)name {
[self willChangeValueForKey:name];
_name = name;
[self didChangeValueForKey:name];
}
从上述代码中,我们可以看到,要实现手动触发KVO,有两个步骤需要实现
步骤一:重写+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key函数,根据需要返回NO步骤二:对于监听的对象属性发生改变时,需要在改变前后调用willChangeValueForKey:和didChangeValueForKey:
KVO底层原理探究
从上述中我们可以看到我们可以根据KVO监听属性的变化,那么在底层是怎么实现的呢?
KVO监听前后对象的变化
我们在监听name前后获取person的class以及其指针地址看下是否有变化,代码如下
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person.name = @"kvo";
NSLog(@"class = %@ address = %p",object_getClass(self.person),self.person);
//对person的name添加kvo监听
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
NSLog(@"class = %@ address = %p",object_getClass(self.person),self.person);
}
打印结果如下从上图中我们看到,
self.person的class前后发生了改变,但是对象地址并没有改变。我们再通过获取isa来验证下。
- 监听之前
- 监听之后
- 监听前后总结:从上面分析可知,在进行
KVO监听后,对象的class即isa指向发生了改变,由IFPerson变成了NSKVONotifying_IFPerson,但是对象的地址没有改变。
NSKVONotifying_IFPerson是什么?
我们知道了在进行KVO监听后class变成了NSKVONotifying_IFPerson,那么NSKVONotifying_IFPerson从何而来?下面我们看下NSKVONotifying_IFPerson的父类是谁。
准备工作
由于superClass是隐藏属性,在外部无法访问,因为我们定义一个结构体,并强转下类型来获得superClass,代码如下
struct cw_objc_class {
Class _Nonnull isa;
Class _Nullable super_class;
};
// 强转一下类型
struct cw_objc_class *newClass = (__bridge struct cw_objc_class *)object_getClass(self.person);
NSLog(@"superClass = %@",newClass->super_class);
从打印结果来看
NSKVONotifying_IFPerson的父类竟然是IFPerson。
NSKVONotifying_IFPerson类实现了哪些方法?
既然runtime在进行KVO监听的时候派生了NSKVONotifying_IFPerson类,那么NSKVONotifying_IFPerson类跟IFPerson类实现方法有什么区别呢?我们打印下监听前后两个类的实现方法区别
#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
NSLog(@"监听之前方法:");
[self printClassAllMethod:object_getClass(self.person)];
//对person的name添加kvo监听
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
NSLog(@"监听之前方法:");
[self printClassAllMethod:object_getClass(self.person)];
打印结果如下从打印监听前后方法列表我们可以看到
NSKVONotifying_IFPerson类有如下变化
- 重写了被监听属性的
setter方法,如setName: - 修改了
isa指针指向的地址,由原来的IFPerson指向了NSKVONotifying_IFPerson - 实现了
dealloc方法 - 增加了一个
_isKOA标识符
isa指针何时重新指向IFPerson?
既然isa指针在被监听后指向了NSKVONotifying_IFPerson类,那么何时重新指向IFPerson呢?答案是在移除监听的时候。为了验证,我们在dealloc中打印下监听前后isa指针的变化。
KVO总结
KVO在监听属性后,runtime会根据对象的类型生成一个NSKVONotifying_XXX(XXX是原来的类)的派生类,该派生类NSKVONotifying_XXX继承于原来的类NSKVONotifying_XXX重写被监听属性的setter方法,但是赋值的时候通过重写的setter内部消息转发还是通过原来类的setter方法来实现KVO在移除监听时将isa指针指向地址由NSKVONotifying_XXX派生类重新指向原来类