• 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";
    打印结果:
     
     
     
     
     
     
     
     
     
     
  • 相关阅读:
    企业网盘针对文件权限管理的技术实现
    学校私有云盘在学校信息化建设中的作用-教学资源库平台
    Mobox 知识管理平台助推市长质量奖
    为勇敢的华裔女子点赞
    一群喵星人把他家包围了。。
    【OI】简单的分块
    【OI】Kruskal & ufs (克鲁斯卡与并查集)
    【OI】向量&矩阵乘法
    【OI】同余方程
    【Ubuntu】某灯图标过大
  • 原文地址:https://www.cnblogs.com/EchoHG/p/7020441.html
Copyright © 2020-2023  润新知