美文网首页
iOS-浅谈OC中的KVO

iOS-浅谈OC中的KVO

作者: 晴天ccc | 来源:发表于2019-06-21 08:47 被阅读0次

目录

  • 简介
  • 基本使用
  • 底层本质探究
  • 总结
  • 其他补充
    ---- 验证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监听流程。

其他补充

  • 验证NSKVONotifying_*子类内部方法

[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方法

  • 验证_NSSetXXValueAndNotify方法内部实现

在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等方法,会根据被添加观察的属性类型决定调用哪个方法。

  • 手动触发KVO

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

[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];

相关文章

  • iOS-浅谈OC中的KVO

    OC对象的类型 Objective-C中的对象,简称OC对象,主要可以分为3种 instance对象(实例对象)c...

  • iOS - KVO

    [toc] 参考 KVO KVC 【 iOS--KVO的实现原理与具体应用 】 【 IOS-详解KVO底层实现 】...

  • swift中KVO和属性观察器

    开篇提醒:OC中的KVO及其KVO的基础知识可参见:深入runtime探究KVO Swift中,原本没有KVO模式...

  • iOS-浅谈OC中的Block

    KVO简介和基本使用 KVO全称是Key-Value Observing,俗称“键值监听”,可用于监听某个对象属性...

  • iOS-浅谈OC中的Runloop

    LLDB是Xcode自带的调试器,可以通过一些命令获得想要的信息 常用打印 读取内存 修改内存中的值 格式 字节大小

  • iOS-浅谈OC中的KVC

    【一】isa指针 思考消息发送过程 了解Runtime的可能知道,调用方法本质上是消息发送: 思考:实例对象方法保...

  • iOS-浅谈OC中的指针

    将OC代码转换为C/C++代码 打开终端,进入main.m文件所在文件夹下执行以下命令: 但是因为不同平台运行,O...

  • iOS-浅谈OC中的isa

    目录 isa结构----nonpointer----has_assoc----has_cxx_dtor----sh...

  • iOS-浅谈OC中的Category

    目录 简介基本使用问题思考最终结论Category的底层结构拓展-Category的底层结构Cagegory的加载...

  • KVO

    今天和大家讨论一下OC中KVO(KeyValueObserving)键值观察 KVO定义 KVO是iOS开发中的一...

网友评论

      本文标题:iOS-浅谈OC中的KVO

      本文链接:https://www.haomeiwen.com/subject/hmseqctx.html