iOS-浅谈OC中的KVO

2019-06-21  本文已影响0人  晴天ccc

目录

  • 简介
  • 基本使用
  • 底层本质探究
  • 总结
  • 其他补充
    ---- 验证NSKVONotifying_*子类内部方法
    ---- 验证_NSSetXXValueAndNotify方法内部实现
    ---- 手动触发KVO

简介

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

基本使用

添加监听:

self.person = [[Person alloc] init];
self.person.age = 10;
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"];
}

点击事件:

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

结果打印:

监听到< Person: 0x283d468c0>的age属性值改变了 - {
    kind = 1;
    new = 20;
    old = 10;
} 

底层本质探究

首先我们思考一下,KVO的功能是对键值进行监听,在数据改变的时候我们收到消息,而属性改变就是set方法,我们从set方法入手进行观察。

方法一通过打印来观察:
    self.person1 = [[Person alloc] init];
    self.person1.age = 1;
    self.person2 = [[Person alloc] init];
    self.person2.age = 2;
    NSLog(@"添加KVO监听之前类变化 - %@ %@", object_getClass(self.person1), object_getClass(self.person2));
    NSLog(@"添加KVO监听之前方法变化 - %p %p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);
    // 给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    NSLog(@"添加KVO监听之后类变化 - %@ %@", object_getClass(self.person1), object_getClass(self.person2));
    NSLog(@"添加KVO监听之后方法变化 - %p %p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);

打印结果:

添加KVO监听之前类变化 - Person Person
添加KVO监听之前方法变化 - 0x100872518 0x100872518
添加KVO监听之后类变化 - NSKVONotifying_Person Person
添加KVO监听之后方法变化 - 0x187802120 0x100872518
方法二:通过增加断点,在控制台输入LLDB命令来观察:

添加KVO前观察person1实例对象isa指针

(lldb) p self.person1.isa
(Class) $2 = Person
  Fix-it applied, fixed expression was: 
    self.person1->isa
(lldb) p self.person2.isa
(Class) $3 = Person
  Fix-it applied, fixed expression was: 
    self.person2->isa
(lldb) 

添加KVO后观察person1实例对象isa指针

(lldb) p self.person1.isa
(Class) $0 = NSKVONotifying_Person
  Fix-it applied, fixed expression was: 
    self.person1->isa
(lldb) p self.person2.isa
(Class) $1 = Person
  Fix-it applied, fixed expression was: 
    self.person2->isa
(lldb) 

观察person1实例对象setAge:方法地址变化

(lldb) p (IMP) 0x100872518
(IMP) $0 = 0x0000000100872518 (0x0000000100872518)
(lldb) p (IMP) 0x187802120
(IMP) $1 = 0x0000000187802120 (Foundation`_NSSetIntValueAndNotify)
(lldb) 
观察结果

通过对实例对象和方法的前后打印可以看出:

实例对象person1添加了KVO,它的setAge:方法的内存地址发生了改变。
实例对象person2因为没有添加KVO所以类和方法都没有发生改变.

结论1:验证了NSKVONotifying_Person类的存在

Person的实例对象添加KVO后,RunTime会在运行过程中创建一个继承于Person的子类NSKVONOtifying_Person,并将Person实例对象isa指针指向这个子类。

结论2:验证_NSSetIntValueAndNotify方法存在

打印结果可知,添加KVOsetAge:方法的内存地址发生了改变,实现是Foundation框架里的_NSSetIntValueAndNotify方法。

总结

Person的实例对象添加KVO后,Runtime会在运行过程中创建一个继承于Person的子类NSKVONOtifying_Person,并将Person实例对象的isa指针指向这个子类。子类内部会将被观察属性的set方法实现替换为_NSSetIntValueAndNotify方法,如图所示:

伪代码:

// 伪代码
- (void)setAge:(int)age {
    // 调用Foundation框架里的方法
    _NSSetIntValueAndNotify();
}

_NSSetIntValueAndNotify内部会调用willChangeValueForKey:、父类setAge:didChangeValueForKey:方法:

// 伪代码
void _NSSetIntValueAndNotify() {
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}

didChangeValueForKey内部会调用observeValueForKeyPath:方法:

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

总结:

当属性的set方法出发后,执行子类内部的_NSSetIntValueAndNotify方法
_NSSetIntValueAndNotify内部调用:
willChangeValueForKey:--->super setAge:--->didChangeValueForKey:
didChangeValueForKey:内部调用:observeValueForKeyPath:
从而完成整个KVO监听流程。

其他补充

[self printClassMethodList:object_getClass(self.person1)];
[self printClassMethodList:object_getClass(self.person2)];

- (void)printClassMethodList:(Class)cls {
    unsigned int count;
    Method *methodList = class_copyMethodList(cls, &count);
    NSMutableString *methodNames = [[NSMutableString alloc] init];
    for (int i = 0; i < count; i++) {
        Method method = methodList[i];
        [methodNames appendString:NSStringFromSelector(method_getName(method))];
        [methodNames appendString:@", "];
    }
    free(methodList);
    
    NSLog(@"%@的方法有%@", NSStringFromClass(cls), methodNames);
}

打印结果:

NSKVONotifying_Person的方法有: setAge:, class, dealloc, _isKVOA
Person的方法有:setAge:, age

由打印结果得出,子类中不仅有set方法,还有class、dealloc、_isKVOA方法

在Person类中添加打印,并重新设置age的值:

- (void)setAge:(int)age {
    _age = age;
    NSLog(@"setAge");
}

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

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

打印结果:

willChangeValueForKey
setAge
didChangeValueForKey - Begin
监听到<Person: 0x600000d5c270>的age属性值改变 - {
            kind = 1;
            new = 20;
            old = 11;
        }
didChangeValueForKey - End

得出_NSSetIntValueAndNotify内部执行顺序是:

willChangeValueForKey:
setAge:
didChangeValueForKey
observeValueForKeyPath:

这里之所以调用_NSSetIntValueAndNotify方法是因为被添加KVO的属性是int类型,Foundation框架内部还有_NSSetDoubleValueAndNotify_NSSetRangeValueAndNotify等方法,会根据被添加观察的属性类型决定调用哪个方法。

由于observeValueForKeyPath:是在didChangeValueForKey:方法内部调用的,所以需要调用didChangeValueForKey:方法来手动触发,且在调用didChangeValueForKey:前需要调用willChangeValueForKey:方法:

[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
上一篇 下一篇

猜你喜欢

热点阅读