注:分析参考 MJ底层原理班 内容,本着自己学习原则记录
1 KVO 的基本使用
1.1 给 Person 的实例对象 person 添加observer,监听 person 的 age 属性
self.person = [[Person alloc] init];
...
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:@"123"];
1.2 当 person 的 age 属性发生变化时,会调用 observer 的回调方法(也就是NSObject(NSKeyValueObserving)分类的方法)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
2 NSKVONotifying_Person
2.1 执行 KVO 的person1对象,它的 isa 所指向的类对象不再是 Person 类对象,而是 NSKVONotifying_Person 类对象
证明方式:
-
断点打印
-
通过 runtime 的 object_getClass() 方法获取实例对象对应的类对象,证明person 实例对象的类对象确实被系统改写了
- 对比证明系统会给添加了 KVO 的实例对象动态生成一个NSKVONotifying_Person类,且让该实例对象的 isa 指向这个新生成的类对象
- 参考《NSObject 对象占用内存、isa/superclass指向、类信息存放》,可知对象方法信息都是存放在类对象(class object)中的,而
实例对象
调用实例方法
的流程就是通过实例对象的 isa 找到其指向的类对象,在类对象的实例方法列表内找到实例方法,然后调用。如下图所示
- 系统改写了执行 KVO 的 person1 实例的 isa指针,让其指向了NSKVONotifying_Person类对象,这就说明,修改 person1的 age 属性时,调用的 setAge: 方法,实际上调用的是 NSKVONotifying_Person类对象里面的setAge:方法,而不是 Person 类对象的setAge:方法。
3 NSKVONotifying_Person 中 setAge: 的内部执行逻辑猜想
-
NSKVONotifying_Person类中的 setAge: 实现方法猜想逻辑
3.1 证明setAge:方法内部调用的是 _NSSetIntValueAndNotify() 方法
-
通过打印 setAge: 方法的地址判断前后变化
- KVO 之前,调用的 setAge: 方法的地址一样
- KVO 之后,调用的 setAge: 方法的地址不同
-
通过LLDB 命令,将方法地址转成名称形式显示
注意:
_NSSetIntValueAndNotify()是会根据监听属性的类型进行变化的,前面 age 属性的描述为@property (assign, nonatomic) int age;
。当 age 属性描述变更为 double 的话,即@property (assign, nonatomic) double age;
,setAge:方法内调用的方法为_NSSetDoubleValueAndNotify()
3.2 证明 NSKVONotifying_Person 的isa 指向什么?(是 Person 的元类对像吗?)
- 第2.1的第2点中说明,person1的 isa 指向 NSKVONotifying_Person,而 person2 的 isa 指向 Person
- 那么NSKVONotifying_Person的 isa 指向?Person 的 isa指向?答案如下图:
NSKVONotifying_Person的元类对象是自己本身,而 Person 的元类对象也是 Person
注意object_getClass(object_getClass(self.person1)) 表示,获取 self.person1的元类对象,即第一次object_getClass(self.person1)是获取 person1的类对象,第二次object_getClass则是获取类对象的元类对象。
3.3 证明 NSKVONotifying_Person 的 superClass 指向什么?(是 Person 类吗?)
关系如下:
3.4 证明 _NSSetIntValueAndNotify()方法内部实现确实如猜想代码中的调用逻辑
- 通过在 NSKVONotifying_Person 的父类 Person 中(3.2中证明了),重写
willChangeValueForKey:
和didChangeValueForKey:
方法,拦截调用顺序,具体代码如图:
-
点击屏幕,改变 person1的 age 属性值,对比 log 日志打印信息
为什么要通过在 Person 中重写上述方法拦截打印来证明呢?
- NSKVONotifying_Person的父类为 Person 类,当在 Person 类类中重写了上述方法,那么子类可以通过 superclass 指针优先找到Person类中方法实现
willChangeValueForKey:
和didChangeValueForKey:
方法是属于NSObject 的NSObject(NSKeyValueObserverNotification)
分类里的方法,此时的NSObject 是 Person 的父类,按照实例方法的查找调用循序来看,当然会优先调用 Person 中重写的方法,而 Person 中的方法仅仅是重写拦截,内部还会通过 super 调用父类的方法,所以对原有的 NSObject 的分类方法是没有影响的。
- 通过log 日志可知,_NSSet***ValueAndNotify 的内部实现为:
- 调用
willChangeValueForKey:
- 调用原来的setter实现
- 调用
didChangeValueForKey:
-didChangeValueForKey:
内部会调用observer
的observeValueForKeyPath:ofObject:change:context:
方法
4 相关题:
4.1 iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
- 利用
Runtime API
动态生成一个子类,并且让instance对象的isa指向这个全新的子类 - 当修改instance对象的属性时,会调用Foundation的
_NSSetXXXValueAndNotify
函数 -
_NSSet***ValueAndNotify
的内部实现为:
- 调用
willChangeValueForKey:
- 调用原来的setter实现
- 调用
didChangeValueForKey:
-didChangeValueForKey:
内部会调用observer
的observeValueForKeyPath:ofObject:change:context:
方法
4.2 如何手动触发KVO?
-
手动调用willChangeValueForKey:和didChangeValueForKey:
4.3 直接修改成员变量会触发KVO么?
-
不会触发KVO
-
直接修改成员变量,没有触发监听方法
文/Jacob_LJ(简书作者)
PS:如非特别说明,所有文章均为原创作品,著作权归作者所有,转载需联系作者获得授权,并注明出处,所有打赏均归本人所有!