iOS中KVO的本质

1. 什么是KVO

KVO(Key-Value Observing),俗称“键值观察/监听”,是苹果提供的一套事件通知机制,允许一个对象观察/监听另一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发KVO的监听方法来通知观察者。

KVO是在MVC应用程序中的各层之间进行通信的一种特别有用的技术。

2. 为什么需要KVO

观察/监听另一个对象指定属性值的改变

3. 怎样使用KVO

提起KVO,相信很多同学都用过。我们可以用KVO监听对象属性值的改变,当属性值发生改变的时候,我们会在监听方法中得到被监听值的改变情况。
我们简单看一下KVO的使用:

#import "ViewController.h"
#import "YZPerson.h"

@interface ViewController ()
@property (strong, nonatomic) YZPerson *p1;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    YZPerson *p1 = [[YZPerson alloc] init];
    self.p1 = p1;
    self.p1.name = @"Jim";
    [self.p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"context"];
}

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

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"keyPath = %@,  object = %@, change= %@, context= %@", keyPath, object, change, context);
}

/**iOS9以后,可以跟通知一样,不用做监听的移除*/
- (void)dealloc
{
    [self.p1 removeObserver:self forKeyPath:@"name"];
}

@end

可以看到打印:

keyPath = name,  object = <YZPerson: 0x600003cbc6a0>, change= {
    kind = 1;
    new = sun;
    old = Jim;
}, context= context

接下来,我们来看下KVO的原理

在程序中,加入p2对象。如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    YZPerson *p1 = [[YZPerson alloc] init];
    YZPerson *p2 = [[YZPerson alloc] init];
    self.p1 = p1;
    self.p2 = p2;
    self.p1.name = @"Jim";
    self.p2.name = @"Jim2";
    [self.p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"context"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.p1.name = @"sun";
    self.p2.name = @"sun2";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"keyPath = %@,  object = %@, change= %@, context= %@", keyPath, object, change, context);
}

运行结果:
keyPath = name,  object = <YZPerson: 0x600003cbc6a0>, change= {
    kind = 1;
    new = sun;
    old = Jim;
}, context= context

运行程序后点击屏幕,可以看到只有p1被监听,而p2并没有被监听。
在点击处打上断点:
在这里插入图片描述

可以看到:
p1的isa指针是NSKVONotifying_YZPerson
p2的isa指针是YZPerson

NSKVONotifying_YZPerson为YZPerson的子类。
p1.name = @"sun";的setName:方法中调用了_NSSetStringValueAndNotify()函数方法;_NSSetStringValueAndNotify()函数中又实现了类似

[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];

[self didChangeValueForKey:@"name"];中实现具体的通知方法。

假如,使用成员变量(非属性)定义name,那么
在代码中使用self.p1->name = @"sun";不会实现KVO监听的。
因为,这种修改值没有走set方法

同样,我们可以利用

[self.p1 willChangeValueForKey:@"name"];
self.p1 -> name = @"name";
[self.p1 didChangeValueForKey:@"name"];

来手动实现KVO监听。

总结

1.KVO是基于runtime实现的;
2.当某个类的对象第一次被观察时,系统就会在运行期间动态的生成该类的子类,在这个子类中重写基类中任何被观察属性的setter方法。子类在被重写的setter方法实现真正的通知机制(YZPerson->NSKVONotifying_YZPerson);

在这里插入图片描述

4. 使用KVO需要哪些注意点?

面试题

1. KVO如果不实现监听方法会怎样?

也就是

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"keyPath = %@,  object = %@, change= %@, context= %@", keyPath, object, change, context);
}

方法不写

会崩溃
An -observeValueForKeyPath:ofObject:change:context: message was received but not handled. Key path: name

2. KVO如果多次调用移除操作会怎样?

在dealloc里面多次调用

- (void)dealloc
{
    [self.person removeObserver:self forKeyPath:@"name"];
    [self.person removeObserver:self forKeyPath:@"name"];
	[self.person removeObserver:self forKeyPath:@"name"];
    NSLog(@"%s", __func__);
}

程序崩溃
because it is not registered as an observer

在这里插入图片描述

崩溃原因:

当你使用KVO注册观察者后,系统会在内部为这个对象生成一个记录-观察者的列表。每当你调用removeObserver:forKeyPath:时,系统会试图在这个列表中找到对应的观察者,并从列表中移除它。如果没有找到这个观察者,系统会抛出异常,并导致程序崩溃。

如何解决:

解决这个问题的方案有很多种,下面列出几种常见的方法:

1、在移除观察者前,先检查是否已经添加过:你可以自己在代码中维护一个标记,用来记录观察者是否已经被添加。只有当标记显示观察者已经添加,才执行移除操作。

if (self.isObserving) {
    [person removeObserver:self forKeyPath:@"name"];
    self.isObserving = NO;
}

2、捕获异常:使用@try/@catch/@finally语句可以捕获并处理可能抛出的异常,以防止程序崩溃。

@try {
    [self.person removeObserver:self forKeyPath:@"name"];
}
@catch (NSException *exception) {
    NSLog(@"Exception: %@", exception);
}
@finally {
    // Cleanup code here, if necessary.
}

3、使用第三方库:有些第三方库,比如KVOController,封装了KVO的用法,可以很好地处理这类问题。使用这些库可以大大减少KVO使用中的坑,并且代码也会更加优雅。

无论使用哪种解决方案,对于 KVO 最重要的一点就是要确保每次添加观察者后,都要在适当的时机移除观察者,防止出现内存泄露或无法预计的行为。

更多学习关于KVO崩溃的例子,请看:
iOS 开发:『Crash 防护系统』(二)KVO 防护

3. KVO能否监听对象?

这个题当时把我问蒙了,KVO不是监听属性值的改变吗?咋能去监听对象?对象有set方法调用吗?
最后试了一把,还真能!

#import "YZButton.h"
@interface ViewController ()
@property (strong, nonatomic) YZPerson *person;
@end

@implementation ViewController

//在MRC下重写set方法
- (void)setPerson:(YZPerson *)person
{
    if (_person != person) {
        [_person release];
        _person = [person retain];
    }
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    YZPerson *person = [[YZPerson alloc] init];
    self.person = person;
    person.name = @"sss";
    
    //self的"person"
    [self addObserver:self forKeyPath:@"person" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"context"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"%@ %@ %@", object, change, context);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person = [[NSObject alloc] init];
}


打印结果:

<ViewController: 0x7ffdf3513b00> {
    kind = 1;
    new = "<NSObject: 0x600000bf8360>";
    old = "<YZPerson: 0x6000009bc3c0>";
} context

能调用的原因是,self的person对象,将self.person对象改变,等于也是调用了set方法,因此,可以进行监听。

4. 如果在项目中对Person类进行了监听,也创建了一个NSKVONotifying_Person类,那么会编译通过么?

编译通过,因为KVO是运行时刻创建的,并不在编译时刻,在编译时刻只有一个NSKVONotifying_Person,所以不报错,可以通过

但是此时KVO起不了作用,即监听属性值,KVO监听方法不执行

一运行控制台会有打印:
KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class

5. 为何创建一个新的子类,在子类里面监听属性值的改变?

不改变原有类的内容,在子类中进行修改

  • 开闭原则
    对修改关闭,对扩展开放

  • 里式替换原则
    父类可以被子类无缝替换,且原有功能不受任何影响
    在KVO监听中,系统重写了setter方法,然后将isa指针指向其子类,悄无声息的子类替换掉父类。


在这里插入图片描述

学习文章:
iOS - 关于 KVO 的一些总结

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值