OC对象 - KVO

OC对象 - KVO

俗称"键值监听" ,用来监听某个属性值的改变

1. KVO基本使用

1.1 简单的KVO

  • 首先我们新建一个iOS的App项目
  • 新建ZSXPerson
less 复制代码
@interface ZSXPerson : NSObject

@property (nonatomic, assign) int age;

@end
less 复制代码
@implementation ZSXPerson

@end
  • viewController 中创建ZSXPerson属性并初始化,然后添加KVO监听实例对象的age变化,接着通过 touchesBegan 点击屏幕的时候修改age值,来触发 KVO
objectivec 复制代码
#import "ViewController.h"
#import "ZSXPerson.h"

@interface ViewController ()

@property (nonatomic, strong) ZSXPerson *person1;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _person1 = [[ZSXPerson alloc] init];
    [_person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"点击了屏幕");
    _person1.age = 20;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"监听到 %@的%@的值发生变化:%@", object, keyPath, change);
}

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

@end
  • 运行项目,点击屏幕,查看控制台输出
  • 现在我们已经使用 KVO 正常监听 person 的 age 值变化

2. 观察给对象添加kvo监听和没有添加kvo监听的区别

2.1 增加person2对象

我们再增加一个person2属性并初始化,同样在点击屏幕的时候,给person2.age也赋值

此时点击屏幕,控制台确实只输出 person1 对象的监听变化

2.2 思考

  • 我们对person1和person2仅仅做了同样了age赋值操作,kvo是怎么做到添加了监听的对象才触发observeValue呢?

3. KVO底层实现探究

3.1 添加KVO前后,setAge方法发生了什么变化?

在添加KVO前后,分别打印一下方法实现的地址 发现person1 的 setAge 方法在添加完 KVO 监听后变化了,person2 是没有变化的

3.2 添加KVO前后,setAge到底走了什么方法

我们打印一下方法

  • 直接打印方法地址看不到具体方法名,使用(IMP)强转一下

可以观察到

  • 未添加KVO监听的 setAge 方法是-[ZSXPerson setAge:]
  • 添加KVO监听后 setAge 方法是 Foundation_NSSetIntValueAndNotify`

3.2.1 其他数据类型

如果我们还有一个double类型的属性 setHeight方法最终会是_NSSetDoubleValueAndNotify

3.3 查看isa

前面学过isa的指向,我们知道对象方法是存放类对象中的,既然两个对象的setAge对象方法不一样,那是不是他们isa指向的类对象也不一样呢

3.3.1 控制台打印

确实他们isa指向的类对象不是同一个,_person1在这边没有显示类对象名称,_person2可以看出来是ZSXPerson

3.4 使用runtime打印类对象

3.4.1通过runtime打印_person1的类对象:NSKVONotifying_ZSXPerson

3.4.2 打印_person1的类对象的superclass

同时还发现,NSKVONotifying_ZSXPerson的superclass就是ZSXPerson

3.5 结论

添加KVO后

  • 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类NSKVONotifying_ClassName
  • 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数

3.6 _NSSetXXXValueAndNotify方法做了什么

_NSSetXXXValueAndNotifyFoundation框架的东西,因为无法拿到Foundation的源码,但是可以通过一些逆向的手段,得到_NSSetXXXValueAndNotify方法实际执行的伪代码

3.6.1 _NSSetXXXValueAndNotify

调用setAge方法时,_NSSetXXXValueAndNotify方法里面做的内容可以认为是这样:

  • self willChangeValueForKey:@"age"\];

  • self didChangeValueForKey:@"age"\];

3.7 NSKVONotifying_ClassName类对象里面还有什么方法

我们遍历打印一下NSKVONotifying_ClassName里面,看看它还有什么方法

scss 复制代码
- (void)printMethodNameOfClass:(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 appendFormat:@"%@", methodName];
        [methodNames appendFormat:@", "];
    }
    
    // 释放
    free(methodList);
    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
}

点击屏幕的时候,我们调用一下打印

3.7.1 setAge:

会调用Foundation_NSSetIntValueAndNotify`

3.7.2 class:

你会发现,对象添加KVO监听后,isa只想了一个新的子类NSKVONotifying_ZSXPerson,但是我们在使用[person1 class]获取类对象的时候,返回的依然是ZSXPerson

这是因为:

苹果底层设计时,为了屏蔽内部实现,让开发者使用过程中,不会突然看到一个异常的东西,避免胡思乱想

我们可以认为NSKVONotifying_ZSXPerson类重写了 class 方法如下

kotlin 复制代码
- (Class)class {
    return [ZSXPerson class];
}

3.7.2 dealloc:

可以认为里面就是做了一些跟KVO释放有关的收尾操作

  • (void)dealloc { // 收尾工作 }

3.7.2 _isKVOA:

返回是否是KVO的相关类

objectivec 复制代码
- (BOOL)_isKVOA {
    return YES;
}

4. 总结

4.1 未使用KVO监听的对象

4.2 使用了KVO监听的对象

4.3 添加KVO后

  • 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类NSKVONotifying_ClassName
  • 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
    • willChangeValueForKey:
    • 父类原来的setter
    • didChangeValueForKey: 内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)

扩展

手动触发KVO

  • 碰过这样一道面试题:使用下划线直接访问成员变量的方式给变量赋值,会不会触发KVO监听?

答案是:不会。因为KVO重写的是set方法(setAge:)。直接给成员变量赋值不会走set方法,因此也不会触发KVO监听

  • 然后又会问,那能不能手动触发KVO监听?

手动触发

只要手动调用实例对象的willChangeValueForKey:didChangeValueForKey方法,就能触发

注意

需要分别调用willChangeValueForKey:didChangeValueForKey方法才会触发KVO

  • 内部实现在执行didChangeValueForKey方法的时候,会判断前面是否执行了willChangeValueForKey:方法,前面有调用过才会触发KVO 监听

@oubijiexi

相关推荐
cykaw259014 分钟前
Linux网络编程
linux
chunfeng—44 分钟前
纯C协程框架NtyCo
linux·c++·后端·协程·ntyco
南川琼语1 小时前
TIME_WAIT状态+UDP概念及模拟实现服务器和客户端收发数据
linux·服务器·udp
兔兔爱学习兔兔爱学习4 小时前
Linux部署ragflow,从安装docker开始~
linux·docker
xbd_zc7 小时前
【Vagrant+VirtualBox创建自动化虚拟环境】Ansible测试Playbook
linux·ubuntu·自动化·ansible·虚拟机·vagrant·virtualbox
lsnm9 小时前
【LINUX操作系统】线程操作
linux·jvm·c++·ubuntu·centos·gnu
FREEDOM_X10 小时前
Ubuntu 20.04 安装 ROS 2 Foxy Fitzroy
linux·ubuntu·机器人
华纳云IDC服务商10 小时前
如何利用Rust提升Linux服务器效率(详细操作指南)
linux·服务器·rust
桦010 小时前
【Linux】g++安装教程
linux·运维·服务器
Once_day11 小时前
Linux之netlink(2)libnl使用介绍(1)
linux·netlink·libnl3