关于KVO看这篇就够了

栏目: IOS · 发布时间: 5年前

内容简介:例如,我们定义一个 YZPerson 类 继承自 NSObject ,里面有name 和 age 两个属性然后在ViewController中,写如下代码执行之后结果为
  • KVO全称KeyValueObserving,俗称 键值监听 ,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。
  • KVC和KVO都属于键值编程而且底层实现机制都是 isa-swizzing
  • KVO和NSNotificationCenter都是iOS中 观察者模式 的一种实现。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
  • KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。

实现原理

  • KVO是通过isa-swizzling技术实现的(这句话是整个KVO实现的重点)。
  • 在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。当修改 instance 对象的属性时,会调用 Foundation框架的 _NSSetXXXValueAndNotify 函数 ,该函数里面会先调用 willChangeValueForKey: 然后调用父类原来的 setter 方法修改值,最后是 didChangeValueForKey:。didChangeValueForKey 内部会触发监听器(Oberser)的监听方法observeValueForKeyPath:ofObject:change:context:
  • 并且将class方法重写,返回原类的Class。

KVO的使用

使用方法

  1. 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。
  2. 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
  3. 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash。

例如,我们定义一个 YZPerson 类 继承自 NSObject ,里面有name 和 age 两个属性

@interface YZPerson : NSObject
@property (nonatomic ,assign) int age;
@property (nonatomic,strong) NSString  *name;
@end

复制代码

然后在ViewController中,写如下代码

- (void)viewDidLoad {
    [super viewDidLoad];
   	//调用方法
    [self setNameKVO];
}

-(void)setNameKVO{
    self.person = [[YZPerson alloc] init];
    // 注册观察者
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    
    [self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
 
}

// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
  self.person.name = @"ccc";

}

-(void)dealloc
{
    // 移除监听
    [self.person removeObserver:self forKeyPath:@"name"];
}

复制代码

执行之后结果为

KVOdemo[11482:141804] 监听到<YZPerson: 0x6000004e8400>的name属性值改变了 - {
    kind = 1;
    new = ccc;
    old = "<null>";
} - 1111- 1111
复制代码

注意点

需要注意的是,上面代码中我们已经移除了监听,如果再次移除的话,就会crash

例如

- (void)viewDidLoad {
    [super viewDidLoad];
   	//调用方法
    [self setNameKVO];
}
-(void)setNameKVO{
   self.person = [[YZPerson alloc] init];
    // 注册观察者
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    
    [self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
       // 移除监听
    [person removeObserver:self forKeyPath:@"name"];
    // 再次移除
     [person removeObserver:self forKeyPath:@"name"];

}
复制代码

移除多次会报错

KVOdemo[9261:2171323] *** Terminating app due to uncaught exception 'NSRangeException', 
reason: 'Cannot remove an observer <ViewController 0x139e07220> for the key path "name" 
from <YZPerson 0x281322f20> because it is not registered as an observer.'
复制代码

如果忘记移除的话,有可能下次收到这个属性的变化的时候,会carsh

所以,我们要保证add和remove是成对出现的

抛出疑问

加入我们又两个YZPerson对象,只监听其中一个

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setNameKVO];
}

-(void)setNameKVO{
    
    self.person = [[YZPerson alloc] init];
    self.person2 = [[YZPerson alloc] init];
    // 注册观察者
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    
    [self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
  

}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.name = @"ccc";
    self.person2.name = @"ddd";

}
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc
{
    // 移除监听
    [self.person removeObserver:self forKeyPath:@"name"];
}
复制代码

点击屏幕时候,打印如下

监听到<YZPerson: 0x600001afa740>的name属性值改变了 - {
    kind = 1;
    new = ccc;
    old = "<null>";
} - 1111
复制代码

但是我们知道,

self.person.name = @"ccc";
 self.person2.name = @"ddd";
复制代码

上面这两句代码都是调用 setName

@implementation YZPerson
- (void)setName:(NSString *)name{
    _name = name;
}
复制代码

也就是说,两个对象,都是调用 setName 方法,根据iOS的机制,应该都是根据 YZPerson的isa指针,去类对象中查找方法。怎么就能做到 self.person.name 可以监听 self.person2.name 不能监听呢?

本质分析

针对上面的疑问,我们可以猜测,是不是person 和 person2 的isa指针不一样呢,导致执行的方法不同呢?

打断点,并打印两者的isa

(lldb) po self.person->isa
NSKVONotifying_YZPerson

(lldb) po self.person2->isa
YZPerson

复制代码

发现果然是isa指针不同,既然isa指向不同了。是不是说明两者的类对象不同呢?答案是肯定的。因为oc中,就是根据isa去查找类对象的,那么接下来进行验证

验证

对类对象进行验证

导入 runtime,对两者的类进行打印

NSLog(@"person添加KVO监听之前 - %@ %@",
          object_getClass(self.person),
          object_getClass(self.person2));
    // 注册观察者
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
    
    NSLog(@"person添加KVO监听之后 - %@ %@",
          object_getClass(self.person),
          object_getClass(self.person2));


复制代码

打印结果为

KVOdemo[13302:171740] person添加KVO监听之前 - YZPerson YZPerson
 KVOdemo[13302:171740] person添加KVO监听之后 - NSKVONotifying_YZPerson YZPerson
复制代码

由此可见,添加KVO监听之后,确实 self.person 的类对象是NSKVONotifying_YZPerson 而self.person2的类对象不变,依然是 YZPerson

注意点:如果使用 [self.person class] 无法获取真实的类

例如我们在添加KVO监听之后,这样来获取类对象

NSLog(@"person添加KVO监听之后 - %@ %@",
              [self.person class],
              [self.person2 class]);

复制代码

那么打印结果为

KVOdemo[17839:239214] person添加KVO监听之后 - YZPerson YZPerson
复制代码

这是因为,苹果为我们生成了中间类 NSKVONotifying_YZPerson 但是,他并不想让我们知道有这个类的存在,重写了这个 NSKVONotifying_YZPerson 的class方法,所以,我们获取的结果是不准确的。

对方法IMP进行验证

我们知道,当改变name属性的时候,是调用setName: 进行的,那我们就来查看一下setName: 有什么变化

NSLog(@"person添加KVO监听之前 - %p %p",
          [self.person methodForSelector:@selector(setName:)],
          [self.person2 methodForSelector:@selector(setName:)]);
    // 注册观察者
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
    
    NSLog(@"person添加KVO监听之后 - %p %p",
          [self.person methodForSelector:@selector(setName:)],
          [self.person2 methodForSelector:@selector(setName:)]);

复制代码

结果为

KVOdemo[13655:177448] person添加KVO监听之前 - 0x10ccfa630 0x10ccfa630
 KVOdemo[13655:177448] person添加KVO监听之后 - 0x10d056d1a 0x10ccfa630

复制代码

有上面打印结果可知,添加监听之后,self.person的 setName 地址变了。继续通过LLDB查看

KVOdemo[13655:177448] person添加KVO监听之前 - 0x10ccfa630 0x10ccfa630
KVOdemo[13655:177448] person添加KVO监听之后 - 0x10d056d1a 0x10ccfa630
(lldb) p (IMP)0x10ccfa630
(IMP) $0 = 0x000000010ccfa630 (KVOdemo`-[YZPerson setName:] at YZPerson.m:12)
(lldb) p (IMP)0x10d056d1a
(IMP) $1 = 0x000000010d056d1a (Foundation`_NSSetObjectValueAndNotify)

复制代码

可知,添加KVO监听之后,setName:方法指向了 Foundation 框架中的 _NSSetObjectValueAndNotify

元类对象验证

既然添加KVO监听之后,类对象不是同一个,那元类对象呢?如下验证

// 注册观察者
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
    
    
NSLog(@"类对象 - %@ %@",
          object_getClass(self.person),  // self.person.isa
          object_getClass(self.person2)); // self.person2.isa
          
 NSLog(@"元类对象 - %@ %@",
          object_getClass(object_getClass(self.person)), // self.person.isa.isa
          object_getClass(object_getClass(self.person2))); // self.person2.isa.isa
          
复制代码

结果为

KVOdemo[13655:177448] 类对象 - NSKVONotifying_YZPerson YZPerson
 KVOdemo[13655:177448] 元类对象 - NSKVONotifying_YZPerson YZPerson

复制代码

可知,元类对象变成了 NSKVONotifying_YZPerson

内部调用流程

那设置了kvo监听之后,内部调用有什么流程呢?我们在Person中添加如下代码

#import "YZPerson.h"

@implementation YZPerson
- (void)setName:(NSString *)name{
     _name = name;   
}

- (void)willChangeValueForKey:(NSString *)key
{
    [super willChangeValueForKey:key];
    
    NSLog(@"willChangeValueForKey");
}

- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"didChangeValueForKey - begin");
    
    [super didChangeValueForKey:key];
    
    NSLog(@"didChangeValueForKey - end");
}

复制代码

点击屏幕之后,如下打印

KVOdemo[17486:233248] willChangeValueForKey
KVOdemo[17486:233248] didChangeValueForKey - begin
KVOdemo[17486:233248] 监听到<YZPerson: 0x600000889ca0>的name属性值改变了 - {
    kind = 1;
    new = ccc;
    old = "<null>";
} - 1111
KVOdemo[17486:233248] didChangeValueForKey - end

复制代码

也就是说调用 [super didChangeValueForKey:key]; 的时候,监听到监听对象的改变,进而处理监听逻辑

窥探 NSKVONotifying_YZPerson 的方法

- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    
    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[i];
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    
    // 释放
    free(methodList);
    
    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
}

-(void)setNameKVO{
    
    self.person = [[YZPerson alloc] init];

    // 注册观察者
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
    NSLog(@"person添加KVO监听之后 - self.person的类是:%@   里面的方法有:",object_getClass(self.person));
    [self printMethodNamesOfClass:object_getClass(self.person)];
}

复制代码

上面代码执行结果为

KVOdemo[19286:259546] person添加KVO监听之后 - self.person的类是:NSKVONotifying_YZPerson   里面的方法有:
KVOdemo[19286:259546] NSKVONotifying_YZPerson setName:, class, dealloc, _isKVOA,
复制代码

这也进一步验证了,系统重写了新建的子类 NSKVONotifying_YZPerson 的setName, class, dealloc,新增了 _isKVOA方法

手动调用KVO

由上面可知,KVO监听的关键 willChangeValueForKeydidChangeValueForKey 起了关键作用,一般来说只有监听属性发生变化的时候,才能触发监听,但是如果我们想自己手动调用KVO的话,只要自己手动调用这两个方法就可以了。eg:

-(void)setNameKVO{
    
    self.person = [[YZPerson alloc] init];
    // 注册观察者
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
    NSLog(@"person添加KVO监听之后 - self.person的类是:%@   里面的方法有:",object_getClass(self.person));
    [self printMethodNamesOfClass:object_getClass(self.person)];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //    self.person.name = @"ccc";
    // 手动调用KVO
    [self.person willChangeValueForKey:@"name"];
    
    [self.person didChangeValueForKey:@"name"];
}
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc
{
    // 移除监听
    [self.person removeObserver:self forKeyPath:@"name"];
}

复制代码

每次点击屏幕的时候,打印如下

监听到<YZPerson: 0x600003e5b020>的name属性值改变了 - {
    kind = 1;
    new = "<null>";
    old = "<null>";
} - 1111
复制代码

可以看到虽然,new 和 old都是null ,也就是name的值没有改变,但是因为我们手动调用了,

[self.person willChangeValueForKey:@"name"];
    
 [self.person didChangeValueForKey:@"name"];
复制代码

所以就是会触发KVO

拓展深入

iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

  • 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数 willChangeValueForKey: 父类原来的setter didChangeValueForKey: 内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)

如何手动触发KVO?

  • 手动调用willChangeValueForKey:和didChangeValueForKey:

直接修改成员变量会触发KVO么?

  • 不会触发KVO

因为,触发KVO是因为,执行set方法时候,调用 willChangeValueForKey didChangeValueForKey 但是直接修改成员变量不会调用set方法

eg: 我们把name 成员变量 设置为如下的形式,就不会自动生成set 和 get方法

@interface YZPerson : NSObject
{
    @public
    NSString *_name;
    
}
@property (nonatomic ,assign) int age;

@end
复制代码

在监听控制器里面,改成如下操作,直接修改成员变量

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    self.person->_name = @"abc";

}

复制代码

这样是不会触发KVO的,如果我们想让它触发KVO,就手动调用,如下

@implementation ViewController

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    [self.person willChangeValueForKey:@"name"];
    self.person->_name = @"abc";
	 [self.person didChangeValueForKey:@"name"];
}

@end
复制代码

这样就可以触发KVO了。

通过KVC修改属性会触发KVO么?

  • 会触发KVO
  • 详细分析见KVC那点儿事

KVC 与 KVO 的不同?

KVC(键值编码),即 Key-Value Coding,一个非正式的 Protocol,使用字符串(键)访问一个对象实例变量的机制。而不是通过调用 Setter、Getter 方法等显式的存取方式去访问。 KVO(键值监听),即 Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,对象就会接受到通知,前提是执行了 setter 方法、或者使用了 KVC 赋值。

KVO和 notification(通知)的区别?

notification 比 KVO 多了发送通知的一步。 两者都是一对多,但是对象之间直接的交互,notification 明显得多,需要notificationCenter 来做为中间交互。而 KVO 如我们介绍的,设置观察者->处理属性变化,至于中间通知这一环,则隐秘多了,只留一句“交由系统通知”,具体的可参照以上实现过程的剖析。

notification 的优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,例如键盘、前后台等系统通知的使用也更显灵活方便。 (参照通知机制第五节系统通知名称内容)


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

软技能

软技能

John Sonmez / 王小刚 / 人民邮电出版社 / 2016-7 / 59.00元

这是一本真正从“人”(而非技术也非管理)的角度关注软件开发人员自身发展的书。书中论述的内容既涉及生活习惯,又包括思维方式,凸显技术中“人”的因素,全面讲解软件行业从业人员所需知道的所有“软技能”。本书聚焦于软件开发人员生活的方方面面,从揭秘面试的流程到精耕细作出一份杀手级简历,从创建大受欢迎的博客到打造你,从提高自己工作效率到与如何与“拖延症”做斗争,甚至包括如何投资不动产,如何关注自己的健康。本......一起来看看 《软技能》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

在线进制转换器
在线进制转换器

各进制数互转换器

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码