zoukankan      html  css  js  c++  java
  • KVO

    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
    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
    输出结果:
     
    一个Demo:
     
    在LYXItem.h文件
    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
    View Code

    在运行文件中

     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";
    打印结果:
     
     
     
     
     
     
     
     
     
     
  • 相关阅读:
    HDU 5486 Difference of Clustering 图论
    HDU 5481 Desiderium 动态规划
    hdu 5480 Conturbatio 线段树 单点更新,区间查询最小值
    HDU 5478 Can you find it 随机化 数学
    HDU 5477 A Sweet Journey 水题
    HDU 5476 Explore Track of Point 数学平几
    HDU 5475 An easy problem 线段树
    ZOJ 3829 Known Notation 贪心
    ZOJ 3827 Information Entropy 水题
    zoj 3823 Excavator Contest 构造
  • 原文地址:https://www.cnblogs.com/EchoHG/p/7020441.html
Copyright © 2011-2022 走看看