KVO的实质

236 阅读5分钟

1、KVO的概念

  • 概念:KVO--Key-Value Observing,又称“键值观察(监听)”,用于监听一个对象的某个属性的改变。

2、KVO的如何使用?

  • 那么,KVO应该如何使用呢?举个例子:我们想监听某个人(对象)的姓名(属性)变更,如果变更了就作出响应,怎么做呢?

1.创建一个Person类,内有部分属性比如: name

#import <Foundation/Foundation.h>
@interface Person : NSObject
@property(nonatomic, copy) NSString *name;
@property(nonatomic, assign) NSInteger age;
@end

2.创建Person实例,并且监听该实例的某属性

#import "Person.h"
@interface ViewController ()
@property(nonatomic, strong) Person *aPerson;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.aPerson = [[Person alloc] init];
    self.aPerson.name = @"小华";
    //监听属性name
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
    [self.aPerson addObserver:self forKeyPath:@"name" options:options context:nil];
}
@end

注:这里需要注意第二个参数 keyPath 需要手动键入字符串类型的属性名,要仔细防止手误。

3.添加监听响应的方法,响应条件为当对象的属性值发生改变时就会响应

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"监听到%@的%@属性发生改变 -> %@", object, keyPath, change);
}

4.对目标对象的属性值进行修改

- (void)changePropertyNameValue {
    self.aPerson.name = @"小明";
}

此方法执行后就会自动调用步骤3的方法。

5.移除监听

- (void)dealloc {
    [self.aPerson removeObserver:self forKeyPath:@"name"];
}

整个流程下来,基本就是实现KVO的整个步骤。 在修改属性值后控制台输出为:

3、KVO的实质是什么?或者说原理?

要看清KVO的实质,不妨先来看看在对某对象的某属性进行键值观察前后有什么区别吧 思路:我们创建Person的两个实例对象,一个personA,一个personB并且对personB的name进行键值观察,然后打印两者的isa指针,看下两者的区别

1.分别创建两个对象personA和personB,并且对后者的属性name KVO。

    //创建实例
    self.personA = [[Person alloc] init];
    self.personA.name = @"小华";
    
    self.personB = [[Person alloc] init];
    self.personB.name = @"小华KVO";
    //监听属性name
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
    [self.personB addObserver:self forKeyPath:@"name" options:options context:nil];

2.查看personA和personB的isa区别

通过对比控制台的输出,我们大致可以看出一些端倪,就是两者 isa 指针的指向略有不同, 我们再用object_getClassName(instance)查看 personA 和 personB 实例区别:

注:通过object_getClass函数拿到的才是isa真正指向的类。

使用 -(IMP)methodForSelector:@selector() 来获取两者实例的实现地址:

没看错的话,添加了 KVO 的 PersonB 在调用 setName: 的时候,调用了 Foundation 的 _NSSetObjectValueAndNotify 方法。

注:methodForSelector:@selector()用来获取某方法的实现地址

那么我们可以通过下图描述在对一个实例添加了 KVO 之后以及修改了被监听的属性值之后(调用setProperty:)这一系列操作:

对于未使用KVO监听的实例对象personA:

对于使用了KVO监听的实例对象personB:

NSKVONotifying_Person 类是利用runtime动态特性的API创建出来的一个 Person 的一个子类,那么,在我们调用 personB 的 setName:时,实际上是调用了 NSKVONotifying_Person_NSSetObjectValueAndNotify()函数的实现,此函数的实现可以参考以下伪代码:

- (void)setName:(NSString *)name {
    _NSSetObjectValueAndNotify();
}

void _NSSetObjectValueAndNotify() {
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
}

- (void)didChangeValueForKey:(NSString *)key {
    // 通知监听器,name属性发生了改变
    [observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}

以上,基本就是 KVO 的实质。

4、如何触发KVO?

以下有8个方法

外部修改 name:

// 1
self.personB.name = @"小明";
// 2
[self.personB setValue:@"小明2" forKey:@"name"];
// 3
[self.personB setValue:@"小明3" forKey:@"_name"];

内部修改 name:

// 4
self.name = @"小明4";
// 5
_name = @"小明5";
// 6
[self setValue:@"小明6" forKey:@"name"];
// 7
[self setValue:@"小明7" forKey:@"_name"];

通过执行结果怎样呢?

1、2、4、6 会触发监听响应,而3、5、7不会触发监听响应。

那么,原因为何呢?

因为在上面我们知道,KVO的本质是通过runtime的isa-swizzling新建了一个子类,并且在 setProperty 的时候调用了这个子类的 _NSSetXXXValueAndNotify() 方法,因为1、2、4、6会走setter方法,而3、5、7直接设置成员变量 name 的话,并没有走setter方法,所以也就无法触发监听。

我们在使用KVC时,key加不加_决定了是否走setter方法。

另外,数组如何去监听呢?

正常KVO监听数组时,即使我们修改了数组的内容(这里指修改数组内部的内容-增删改)因为没有触发setter方法所以不会触发监听,最简单的就是加一句self.arr = self.arr就可以触发监听了。

5、怎样可以手动触发 KVO 响应?

如果一个类想要实现手动的 KVO 监听和响应,则必须重写NSObject实现的automaticallyNotifiesObserversForKey:方法,并对需要实现手动发送的key返回NO,其余则调用super。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"property"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

然后在 property 的值改变之前调用 willChangeValueForKey:,在值改变之后调用 didChangeValueForKey:。当然,在什么样的情况下才调用这两个方法,是由我们程序员(或者说是需求场景)的逻辑决定的。

如果一个操作造成了多个key的值的改变,则willChangeValueForKey:didChangeValueForKey:必须嵌套着调用,示例如下:

- (void)setProperty:(id)theProperty {
    [self willChangeValueForKey:@"theProperty"];
    [self willChangeValueForKey:@"theProperty2"];
    _theProperty = theProperty;
    _theProperty2 = _theProperty2+1;
    [self didChangeValueForKey:@"theProperty2"];
    [self didChangeValueForKey:@"theProperty"];
}

willChangeValueForKey:didChangeValueForKey:调用时都会调用 valueForKey:,并把得到的结果分别当成old value和new value,以告知观察者,猜测系统就是用看这两个方法来发送通知的。

6、小结

  • 1.KVO的实质

KVO- Key-Value Observing 是利用addObserver:forKeyPath:options:context:在对一个需要观察的属性property注册一个观察者以后,利用Runtime的isa-swizzling新建了一个该对象class的子类 NSKVONotifying_ObjectClass, 并将该对象的isa指针指向这个子类,在调用该对象的 setPropertyName: 方法的时候,会调用这个子类的_NSSetXXXValueAndNotify()方法,在此方法里,分别调用willChangeValueForKey: 、父类的setPROPERTYNAME:方法和didChangeValueForKey:

  • 2._NSSet**ValueAndNotify()的实现:

[self willChangeValueForKey:@"PROPERTY_NAME"];(该方法会发出通知)

[super setPROPERTY_NAME:newPropertyValue];

[self didChangeValueForKey:PROPERTY_NAME];(该方法会发出通知)

  • 3.几个有用的基础的方法

利用object_getClassName(instance) 可以获取 instance 的 isa真正的指向(实例对象的isa指向的是)。

对前文例子利用 LLDB 调试,若 po objet_getClassName(self.personB) 得到的是 NSKVONotifyingPerson。

利用[instance methodForSelector:@selector()] 可以获取到某实例在调用某方法时该方法的实现地址IMP。

对前文的例子利用 LLDB 调试,若 po [self.personB methodForSelector:@selector()] 得到的是 Foundation 框架下的 (Foundation_NSSetObjectValueAndNotify)` 。