iOS 底层 day04 KVO
2020-08-25 本文已影响0人
望穿秋水小作坊
一、基础 KVO 的日常使用
一般情况,分如下三个步骤:
// 1. 进行监听
[self.person1 addObserver:self forKeyPath:@"height" options:3 context:@"123"];
// 2. 添加监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"是监听到了%@实例对象的%@值,改变了%@,context:%@", object, keyPath, change, context);
}
// 3. 销毁监听
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"height"];
}
二、探求 KVO 的本质,并从代码层面上求证
1. 先说结论,如下图
KVO 原理图- KVO 对象生成了一个 继承自
SPPerson
类对象的NSKVONotifying_SPPerson
类对象 - 并将实例对象的 isa 指向
NSKVONotifying_SPPerson
类对象 - 并且有四个方法,
setHeight:
、class
、dealloc
、_isKVOA
- 用图中的伪代码,重写了
setHeight:
方法
2. 代码观察被 KOV 实例对象的 isa 指针
- 编写如下代码,并打上断点
- (void)viewDidLoad {
[super viewDidLoad];
SPPerson *person1 = [[SPPerson alloc] init];
person1.height = 1;
SPPerson *person2 = [[SPPerson alloc] init];
person2.height = 2;
NSLog(@"请在此处打断点1");
[person1 addObserver:self forKeyPath:@"height" options:3 context:@"123"];
NSLog(@"请在此处打断点2");
}
- 我们可以获得如下调试信息
(lldb) p person1->isa
(Class) $0 = SPPerson
(lldb) p person2->isa
(Class) $1 = SPPerson
2020-08-25 15:42:17.983376+0800 Demo-KVO[9298:415101] 请在此处打断点1
(lldb) p person1->isa
(Class) $2 = NSKVONotifying_SPPerson
(lldb) p person2->isa
(Class) $3 = SPPerson
- 我们发现在 KOV 之后,person1 的 isa 指针,指向了一个
NSKVONotifying_SPPerson
的类对象
3. 代码观察NSKVONotifying_SPPerson
类对象方法列表
- (void)viewDidLoad {
[super viewDidLoad];
SPPerson *person1 = [[SPPerson alloc] init];
person1.height = 1;
NSLog(@"KVO 之前 %@ 的方法列表:", object_getClass(person1));
[self printClassMethods:object_getClass(person1)];
[person1 addObserver:self forKeyPath:@"height" options:3 context:@"123"];
NSLog(@"KVO 之后 %@ 的方法列表:", object_getClass(person1));
[self printClassMethods:object_getClass(person1)];
}
- (void)printClassMethods:(Class) cls {
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [[NSMutableString alloc] init];
for (int i = 0 ; i < count; i++) {
NSString *name = NSStringFromSelector(method_getName(methodList[i]));
[methodNames appendString:name];
[methodNames appendString:@" , "];
}
NSLog(@"%@", methodNames);
}
- 代码输出信息如下:
Demo-KVO[10337:501248] KVO 之前 SPPerson 的方法列表:
Demo-KVO[10337:501248] height , setHeight: , weight , setWeight: ,
Demo-KVO[10337:501248] KVO 之后 NSKVONotifying_SPPerson 的方法列表:
Demo-KVO[10337:501248] setHeight: , class , dealloc , _isKVOA ,
- 由此可以看出
NSKVONotifying_SPPerson
有setHeight: , class , dealloc , _isKVOA
这四个方法 - 思考为什么没有
height
方法?因为它的父类有,所以它不需要多此一举
4. 代码观察被 KOV 实例对象的 set 方法
- (void)viewDidLoad {
[super viewDidLoad];
SPPerson *person1 = [[SPPerson alloc] init];
person1.height = 1;
NSLog(@"%p",[person1 methodForSelector:@selector(setHeight:)]);
[person1 addObserver:self forKeyPath:@"height" options:3 context:@"123"];
NSLog(@"%p",[person1 methodForSelector:@selector(setHeight:)]);
}
- 得到如下信息
Demo-KVO[9473:425751] 0x1010eaf60
Demo-KVO[9473:425751] 0x7fff258f10eb
(lldb) p (IMP)0x1010eaf60
(IMP) $1 = 0x00000001010eaf60 (Demo-KVO`-[SPPerson setHeight:] at SPPerson.h:15)
(lldb) p (IMP)0x7fff258f10eb
(IMP) $2 = 0x00007fff258f10eb (Foundation`_NSSetIntValueAndNotify)
- 说明 KVO 之后
setHeight:
方法被替换了成了_NSSetIntValueAndNotify
方法
5. _NSSetIntValueAndNotify
是一个 C 语言方法,因为 Foundation
框架并不开源,所以我们只能从侧面猜测它的内部实现。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.person1 willChangeValueForKey:@"height"];
[self.person1 didChangeValueForKey:@"height"];
}
- 打印如下
2020-08-25 16:48:15.816554+0800 Demo-KVO[10176:485066] 是监听到了<SPPerson: 0x600003eb0230>实例对象的height值,改变了{
kind = 1;
new = 1;
old = 1;
},context:123
- 我们实现上述代码,我们发现可以不直接改变 person1 的height 的数值,也可以触发
- (void)observeValueForKeyPath:ofObject:change:context:
- 所以我们可以猜测,KVO 实现
setHeight:
方法的伪代码如下:
- (void)setHeight:(int)height {
[self willChangeValueForKey:@"height"];
[super setHeight:value];
[self didChangeValueForKey:@"height"];
}
三、两道经典面试题
1. iOS 用什么方式实现对一个对象的 KVO?(KVO 的本质是什么?)
- 利用 RuntimeAPI 动态生成一个子类,并且让 instance 对象的 isa 指针指向这个全新子类
- 这个全新子类会重写 KVO 属性的
set
方法,伪代码如下: - willChangeValueForKey:
- 父类 setter 方法
- didChangeValueForKey:
2. 如何手动触发 KVO?
- willChangeValueForKey:
- didChangeValueForKey: