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:供运行期使用的一些位标识。
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 方法之前和之后,通知所有观察对象属性值的更改情况。
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 //断点3logs_code_collapse">View Code
运行项目,
断点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指针,这个指针指向的一定是个这个对象的类对象
由于KVO内部实现的原理是重写了set方法,因此只有当被观察对象的属性调用set方法赋值的时候才会执行KVO的的回调方法。所以如果直接对属性的成员变量直接赋值那么不会触发KVO。
1.注册观察者
2.在回调方法中处理事件
3.移除观察者
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 }View Code
输出结果:
imageView2/2/w/1240" alt="" data-original-src="http://upload-images.jianshu.io/upload_images/1809729-3224439aad11d95c.png?imageMogr2/auto-orient/strip%7CimageView2/2">