1.KVO概念
KVO(Key - Value - Observing)即键值观察,它提供一种机制,当被观察的对象的属性发生改变后,对象会接收到通知,从而做出相应的改变。
2.KVO实现原理
这里要说一个isa指针,在Objective-C中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别。任何对象都有isa指针。
那么什么是类呢?在xcode中用快捷键Shift+Cmd+O 打开文件objc.h 能看到类的定义:
可以看出:
Class 是一个 objc_class 结构类型的指针, id是一个 objc_object 结构类型的指针.
我们再来看看 objc_class 的定义:
稍微解释一下各个参数的意思:
isa:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。
super_class:父类,如果该类已经是最顶层的根类,那么它为NULL。
version:类的版本信息,默认为0
info:供运行期使用的一些位标识。
instance_size:该类的实例变量大小
ivars:成员变量的数组
再来看看各个类实例变量的继承关系:
每一个对象本质上都是一个类的实例。其中类定义了成员变量和成员方法的列表。对象通过对象的isa指针指向类。
每一个类本质上都是一个对象,类其实是元类(meteClass)的实例。元类定义了类方法的列表。类通过类的isa指针指向元类。
所有的元类最终继承一个根元类,根元类isa指针指向本身,形成一个封闭的内循环。
原理:每一个对象都有一个isa指针,这个对象根据isa指针去寻找它所归属的类,当我们给一个对象注册观察者的时候,系统会在运行时给这个对象创建一个子类,这个子类继承于当前对象归属的类,并把当前对象的isa指针指向这个子类,于是当前对象就变成了这个子类的一个实例。那么这个子类内部做了什么操作呢?其实这个子类重写了set方法,当原对象在调用set方法赋值的时候,会根据isa指针到新建子类的方法列表去寻找set方法的IMP,此时这个重写的set方法会对所有观察这个属性的对象发出通知,于是原有的对象会作出改变。
深入剖析:
Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为: NSKVONotifying_A的新类,该类继承自对象A的本类,且KVO为NSKVONotifying_A重写观察属性的setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
- NSKVONotifying_A类剖析:在这个过程,被观察对象的 isa 指针从指向原来的A类,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_A类,来实现当前类属性值改变的监听;
- 所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类(),就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A的中间类,并指向这个中间类了。
- 因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。
- KVO键值观察依赖于NSObject的两个方法:willChangeValueForKey和didChangevlueForKey,即在键值改变前后分别调用这两个方法,然后在这两个方法的中间调用父类set方法赋值。
- 被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;之后observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
KVO为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
1 -(void)setName:(NSString *)newName 2 { 3 [self willChangeValueForKey:@"name"]; //KVO在调用存取方法之前总调用 4 [super setValue:newName forKey:@"name"]; //调用父类的存取方法 5 [self didChangeValueForKey:@"name"]; //KVO在调用存取方法之后总调用 6 }
示例验证
1 //Person类 2 @interface Person : NSObject 3 @property (nonatomic,copy) NSString *name; 4 @end 5 6 //controller 7 Person *per = [[Person alloc]init]; 8 //断点1 9 [per addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil]; 10 //断点2 11 per.name = @"小明"; 12 [per removeObserver:self forKeyPath:@"name"]; 13 //断点3
运行项目,
- 在
断点1
位置:
- 可以看到
isa
指向Person
类,我们也可以使用lldb
命令查看:(lldb) po [per class] Person (lldb) po object_getClass(per) Person (lldb)
- 在
断点2
位置:
(lldb) po [per class] Person (lldb) po object_getClass(per) NSKVONotifying_Person (lldb)
- 在
断点3
位置:
(lldb) po [per class] Person (lldb) po object_getClass(per) Person (lldb)
上面的结果说明,在per对象被观察时,framework使用runtime动态创建了一个Person类的子类NSKVONotifying_Person,而且为了隐藏这个行为,NSKVONotifying_Person重写了- class方法返回之前的类,就好像什么也没发生过一样。但是使用object_getClass()时就暴露了,因为这个方法返回的是这个对象的isa指针,这个指针指向的一定是个这个对象的类对象
3.KVO的特点
由于KVO内部实现的原理是重写了set方法,因此只有当被观察对象的属性调用set方法赋值的时候才会执行KVO的的回调方法。所以如果直接对属性的成员变量直接赋值那么不会触发KVO。
4.KVO的调用步骤
1.注册观察者
2.在回调方法中处理事件
3.移除观察者
5.代码实践
1 self.changeStr = @"您好"; 2 [self addObserver:self forKeyPath:@"changeStr" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil]; 3 self.changeStr = @"大家都好"; 4 5 6 -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context 7 { 8 NSLog(@"被改变的属性是%@",keyPath); 9 NSString *str = [change objectForKey:NSKeyValueChangeNewKey]; 10 NSString *odlStr = [change objectForKey:NSKeyValueChangeOldKey]; 11 NSLog(@"旧属性是%@",odlStr); 12 NSLog(@"新属性是%@",str); 13 }
输出结果:
1 #import <Foundation/Foundation.h> 2 3 @interface LYXItem : NSObject 4 5 @property(nonatomic, copy) NSString *name; 6 @property(nonatomic, copy) NSString *price; 7 8 @end
在LYXItemView.h文件
1 #import <Foundation/Foundation.h> 2 #import "LYXItem.h" 3 4 @interface LYXItemView : NSObject 5 6 @property(nonatomic, weak) LYXItem *item; 7 8 - (void) showItemInfo; 9 10 @end
在LYXItemView.m中
1 #import "LYXItemView.h" 2 3 @implementation LYXItemView 4 5 @synthesize item = _item; 6 7 - (void)showItemInfo 8 { 9 NSLog(@"item名为:%@, 价格为: %@",self.item.name, self.item.price); 10 } 11 12 13 - (void)setItem:(LYXItem *)item 14 { 15 self -> _item = item; 16 //为item添加监听器,监听item的name的属性的变化 17 [self.item addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil]; 18 19 [self.item addObserver:self forKeyPath:@"price" options:NSKeyValueObservingOptionNew context:nil]; 20 } 21 22 23 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context 24 { 25 NSLog(@"---------------------------observeValueForKeyPath------------------------"); 26 NSLog(@"被修改的keyPath为:%@",keyPath); 27 NSLog(@"被修改的对象为:%@",object); 28 NSLog(@"新被修改的属性值是:%@",[change objectForKey:@"new"]); 29 NSLog(@"被修改的上下文是:%@",context); 30 } 31 32 33 @end
在运行文件中
1 LYXItem *item = [[LYXItem alloc] init]; 2 item.name = @"IOS"; 3 item.price = @"6888"; 4 5 LYXItemView *lyxView = [[LYXItemView alloc] init]; 6 lyxView.item = item; 7 [lyxView showItemInfo]; 8 9 // 更改item的值,触发监听器的方法 10 item.name = @"Android"; 11 item.price =@"1999";