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

相关推荐
运维成长记3 分钟前
Top 100 Linux Interview Questions and Answers
linux·运维·服务器
人工智能训练师27 分钟前
openEuler系统中如何将docker安装在指定目录
linux·运维·服务器·人工智能·ubuntu
百里晴鸢1 小时前
别再混淆!Linux硬链接与软链接的5大关键区别
linux·操作系统
norsd1 小时前
Linux CentOS 安装 .net core 3.1
linux·centos·.netcore
想学c啊啊1 小时前
【Linux】信号(二):Linux原生线程库相关接口
linux·运维·服务器
刘一说2 小时前
CentOS 8开发测试环境:直接安装还是Docker更优?
linux·服务器·docker·centos
AOwhisky2 小时前
7. if 条件语句的知识与实践
linux·运维·云计算·运维开发·shell·选择结构
陌上花开缓缓归以2 小时前
linux cma内存分析
linux
2302_799525742 小时前
【ansible】2.实施ansible playbook
linux·运维·ansible
刘一说3 小时前
Win/Linux笔记本合盖不睡眠设置指南
linux·运维·stm32·电脑