KVO_移动开发_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > 移动开发 > KVO

KVO

 2017/6/16 5:31:07  EchoHG  程序员俱乐部  我要评论(0)
  • 摘要:1.KVO概念KVO即键值观察,它提供一种机制,当被观察的对象的属性发生改变后,对象会接收到通知,从而做出相应的改变。2.KVO实现原理这里要说一个isa指针,在Objective-C中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别。任何对象都有isa指针。那么什么是类呢?在xcode中用快捷键Shift+Cmd+O打开文件objc.h能看到类的定义:可以看出:Class是一个objc_class结构类型的指针,id是一个objc_object结构类型的指针
  • 标签:

1.KVO概念

KVO即键值观察,它提供一种机制,当被观察的对象的属性发生改变后,对象会接收到通知,从而做出相应的改变。

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
logs_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指针,这个指针指向的一定是个这个对象的类对象

 

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 }
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">
  • 相关文章
发表评论
用户名: 匿名