KVO的本质是什么?

473 阅读7分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。


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

如何手动触发KVO?

KVO的全称是Key-Value Observing, 俗称“键值监听”,可以用于监听某个对象属性值的改变。

KVO监听基本使用。例子代码:

创建一个Person类

@interface Person : NSObject
@property(nonatomic, assign) int age;
@end


@implementation Person
@end

在控制器中使用创建person对象,并在点击屏幕的时候, 改变person的age 属性的值。

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

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc] init];
    person.age = 9;
    self.person = person;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person.age = 10;
}

如果我们想在person的age属性值,发生变化的时候。通知控制器做一些相应的处理。也就是说我们找一个监听者,监听person 的 age属性值的变化。该怎么实现。

通常的做法是:

// 给person对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

[self.person addObserver:self forKeyPath:@"age" options:options context:nil];

并在监听者的类中写下回调方法:

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

注意:在不使用监听的时候,要销毁:

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

这个时候,我们给person 新增一个属性,并让控制器监听新增的属性。比如height。这时候,我们就可以增加监听就好。

1. @property(nonatomic, assign) int height;

2. 

Person *person = [[Person alloc] init];
    person.age = 9;

    person.height = 11;

    self.person = person;
3.

 [self.person addObserver:self forKeyPath:@"height" options:options context:nil];


4.

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event

{

    self.person.age = 10;

    self.person.height = 50;

}

这个时候,我们点击屏幕,检测到调用两次KVO

// 当监听对象的属性值发生改变时,就会调用

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context

{

    NSLog(@"监听到%@的%@属性值改变了 -%@",object, keyPath, change);

}

context:(void *)context  这个参数我们之前传递的nil

[self.person addObserver:self forKeyPath:@"age" options:options context:nil];

如果我们修改一下代码:

[self.person addObserver:self forKeyPath:@"age" options:options context:@"333"];

[self.person addObserver:self forKeyPath:@"height" options:options context:@"444"];

当我们监听到context方法参数 会带过来。

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

}

到此为止,KVO的基本用法,就这些。接下来,说一下KVO本质 我们创建person2。并在点击屏幕时,改变person2的属性值,但是我们不监听 person2的属性变化。

Person *person2 = [[Person alloc] init];
    self.person2 = person2;
    self.person2.age = 15;

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person.age = 10;
    self.person2.age = 10;
}

重写Person的setAge,setHeight方法。打断点监听发现。

@implementation Person
- (void)setAge:(int)age

{
    NSLog(@"%d",age);
    _age = age;
}
@end

我们发现 person2 person 在对age属性赋值的时候,都调用了setAge:方法。

但是监听到了person对象age属性改变时的observeValueForKeyPath:监听回调。 由于person2的age属性没有被监听,所以不会调用observeValueForKeyPath。。。。。。

// 当监听对象的属性值发生改变时,就会调用

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context

{

    NSLog(@"监听到%@的%@属性值改变了 -%@",object, keyPath, change);

}

为什么监听到了person属性的改变

没有监听到person2属性的改变呢?

我们发现他们改变age属性的时候,都是通过调用setAge方法改变的。

可是怎么就一个通过KVO监听到了,另一个

没有监听到呢?

本质是究竟是什么呢?

问题不是出现在setAge方法上, 因为大家调用的方法都是一样的。

问题可能出现在person对象上。

我打断点发现:

person的isa指针指向了NSKVONotifying_Person

(lldb) p self.person.isa**

(Class) $1 = NSKVONotifying_Person

  Fix-it applied, fixed expression was: 

    self.person->isa


person2的isa指针指向了Person:

(lldb) p self.person2.isa**

(Class) $2 = Person

  Fix-it applied, fixed expression was: 

    self.person2->isa

前边的文章中,我们说过isa指向实例对象的类对象。我们发现

person,person2 的isa指针所指向的类对象不一样。

同样的类创建出来的对象,类对象为什么不一样了呢?

我们可以猜测,应该我们用KVO监听了person的属性变化, 没有用

KVO监听person2的属性变化。

未使用KVO监听的对象:

person的isa指针指向了Person类对象。

屏幕快照 2021-10-13 下午11.22.28.png

person2的isa指向了NSKVONotifying_Person类对象。

屏幕快照 2021-10-13 下午11.23.06.png

NSKVONotifying_Person是使用Runtime动态创建的一个类,是Person的子类。

NSKVONotifying_Person类的伪代码:

@interface NSKVONotifying_Person : Person

@end

@implementation NSKVONotifying_Person

- (void)setAge:(int)age

{
    _NSSetIntValueAndNotify();
}


void _NSSetIntValueAndNotify()

{

    [self willChangeValueForKey:@"age"];

    [super setAge:age];

    [self didChangeValueForKey:@"age"];

}


- (void)didChangeValueForKey:(NSString *)key

{

    // 通知监听器,某某属性值发生了改变

    [observer observeValueForKeyPath:key ofObject:self change:nil context:nil];

}
@end

这就是为什么person会收到 属性值改变时的observeValueForKeyPath:....调用。而person2不会收到属性值的改变。

内部执行逻辑验证:

写代码验证一下,

NSLog(@"person添加KVO之前-%@,%@",object_getClass(self.person),object_getClass(self.person2));

    // 给person对象添加KVO监听

    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

    [self.person addObserver:self forKeyPath:@"age" options:options context:nil];

    NSLog(@"person添加KVO之后-%@,%@",object_getClass(self.person),object_getClass(self.person2));
nstance对象添加KVO之前的类对象。

instance对象添加KVO之后的类对象。


**2018-06-09 17:35:18.186795+0800 KVO****原理** **[2701:80467] person****添加****KVO****之前****-Person,Person**

**2018-06-09 17:35:21.410571+0800 KVO****原理** **[2701:80467] person****添加****KVO****之后****-NSKVONotifying_Person,Person**

**person 添加KVO监听之后,从Person变为**NSKVONotifying_Person****

**测试一下person添加KVO之后setAge方法的实现地址是否改变。**
NSLog(@"person添加KVO之前-%p,%p",[self.person methodForSelector:@selector(setAge:)],[self.person2 methodForSelector:@selector(setAge:)]);
    // 给person对象添加KVO监听

    NSKeyValueObservingOptions optionsNSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

    [self.person addObserver:self forKeyPath:@"age" options:options context:nil];

    NSLog(@"person添加KVO之后-%p,%p",[self.person methodForSelector:@selector(setAge:)],[self.person2 methodForSelector:@selector(setAge:)]);
**2018-06-09 17:47:27.560033+0800 KVO****原理** **[2937:100797] person****添加****KVO****之前****-0x10f95c570,0x10f95c570**

**2018-06-09 17:47:29.195660+0800 KVO****原理** **[2937:100797] person****添加****KVO****之后****-0x10fd01f8e,0x10f95c570**

**实现的地址改变了。但是实现到底是什么呢?怎么查看方法的具体实现?**

**2018-06-09 17:50:09.292854+0800 KVO****原理** **[3027:107515] person****添加****KVO****之前****-0x1005b0570,0x1005b0570**

**2018-06-09 17:50:11.988789+0800 KVO****原理** **[3027:107515] person****添加****KVO****之后****-0x100955f8e,0x1005b0570**

**(lldb) p (IMP) 0x1005b0570**

(IMP) $0 = 0x00000001005b0570 (KVO原理`-[Person setAge:] at Person.m:12)

**(lldb) p (IMP) 0x100955f8e**

(IMP) $1 = 0x0000000100955f8e (Foundation`_NSSetIntValueAndNotify)

**\
**我们发现,person添加KVO之后,setAge:方法的实现,是在Foundation模块中的 _NSSetIntValueAndNotify。 由于Foundation模块是不开源的,我们目前不能知道其中的源码。


如果你有一台越狱手机。并且有破解经验,你可以查看Foundation编译后的文件。利用反编译工具,查看汇编代码。

_NSSetIntValueAndNotify()确实是存在于Foundation框架中。**

**并且,你会发现:**

**不至有_NSSetIntValueAndNotify**

**而且会有_NSSetDoubleValueAndNotify**

**_NSSetObjectValueAndNotify**\


**_NSSetFloatValueAndNotify**

**等等,主要看你的属性的类型是什么。底层就调用不同的方法。**


**_NSSet*ValueAndNotify内部实现是怎么样的呢?**

**大概格式就是:**

**[self willChangeValueForkey:@"age"];**

**// 原来的setter实现**

****

**[self didChangeValueForkey:@"age"];**

****didChangeValueForkey****

**内部会调用observer 的 observerValueForKeyPath:ofObject:**\


**change:context:方法。**


可以在person中重写

- (void)willChangeValueForKey:(NSString *)key

{
      [super willChangeValueForKey:key ];
      NSLog("willChangeValueForKey");
}


- (void)didChangeValueForKey:(NSString *)key
{
      NSLog("didChangeValueForKey- start");

      // 【super didChangeValueForKey】 内部会调用 

**observerValueForKeyPath:ofObject:**\

**change:context:方法。**

      [super didChangeValueForKey:key ];

      NSLog("didChangeValueForKey - end");

}

来测试。


另外NSKVONotifying_Person类

除了重写了setAge:方法,还重写了

class方法和dealloc方法,及_isKVO

方法。
@implementation NSKVONotifying_Person

- (void)setAge:(int)age

{
    _NSSetIntValueAndNotify();

}

// 屏幕内部实现,隐藏了NSKVONotifying_Person类的存在

- (Class)class

{
    return [Person class];
}

- (void)dealloc

{
    // 收尾工作
}

- (BOOL)_isKVOA

{
    return YES;
}

@end


怎么证明,

NSKVONotifying_Person类

除了重写了setAge:方法,还重写了

class方法和dealloc方法,及_isKVO

方法???

运用运行时,runtime来验证。
- (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);

}
[self printMethodNamesOfClass:object_getClass(self.person)];
[self printMethodNamesOfClass:object_getClass(self.person2)];
打印结果:
**NSKVONotifying_Person setAge:, class, dealloc, _isKVOA,**
**Person setAge:, age,**


iOS用什么方式实现对一个对象的KVO?
(KVO的本质是什么?)
利用RuntimeAPI 动态生成一个子类,
并且让instance对象的isa指向这个全新的子类。
当修改instance对象的属性时,会
调用Foundation的_NSSetXXXValueAndNotify函数
willChangeValueForKey:
父类原来的setter
didChangeValueForKey:
内部会触发监听器(Obsever)的
监听方法(obseverValueForKeyPath:ofObject:change:context:)

如何手动触发KVO?
KVO一般是自动触发的,就算没有人去修改Person对象的属性
我也想去执行 触发逻辑,直接调用下面的方法。
willChangeValueForKey:
didChangeValueForKey:
直接修改成员变量会触发KVO吗?

不会触发KVO.