内容简介:KVO(Key-Value Observing)是 iOS 开发中常用的一种用于监听某个对象属性值变化的技术。下文将以一段示例代码来分析 KVO 的底层原理。示例对通过上述代码可以发现,一旦
KVO(Key-Value Observing)是 iOS 开发中常用的一种用于监听某个对象属性值变化的技术。下文将以一段示例代码来分析 KVO 的底层原理。 源码地址
示例源码
- (void)viewDidLoad { [super viewDidLoad]; [self setupSubviews]; BAOPerson *p1 = [[BAOPerson alloc] init]; BAOPerson *p2 = [[BAOPerson alloc] init]; p1.age = 1; p1.age = 2; p2.age = 2; // self 监听 p1 的 age 属性 NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [p1 addObserver:self forKeyPath:@"age" options:options context:nil]; p1.age = 10; [p1 removeObserver:self forKeyPath:@"age"]; } - (void)setupSubviews { [self setupHeaderView]; } - (void)setupHeaderView { self.headerView.title = @"KVO"; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"监听到 %@ 的 %@ 改变了 %@", object, keyPath,change); }
示例对 p1
进行了 KVO 监听,当 p1
发生改变,即调用 observeValueForKeyPath
方法,从而打印以下信息。
监听到 <BAOPerson: 0x600003750200> 的 age 改变了 { kind = 1; new = 10; old = 2; }
KVO 实现原理
通过上述代码可以发现,一旦 age
属性的值发生改变,就会通知到监听者。我们知道赋值操作都是调用 set
方法,我们可以重写 BAOPerson
类中 age
的 set
方法,观察 KVO 是否是在 set
方法内部做了一些操作来通知监听者。
- (void)setAge:(NSInteger)age { NSLog(@"override setAge"); _age = age; }
我们发现即使重写了 set
方法, p1
除了调用 set
方法之外还会执行监听者的 observeValueForKeyPath
方法。
根据上述实验推测:KVO 在运行时对 p1
对象进行了改动,使 p1
对象在调用 setAge
方法时做了一些额外的操作。所以问题出在对象身上,两个对象可能本质上并不一样。下面我们来探索一下 KVO 内部是如何实现的。
KVO 实现分析
首先分别在添加 KVO 前后打上断点,以观察添加 KVO 前后 p1
对象有何不同。
通过打印对象的 isa
指针,我们发现, p1
对象的 isa
指针由之前的指向类对象 BAOPerson
变成了指向类对象 NSKVONotifying_BAOPerson
。相应地, p2
对象没有改变。因此我们可以推测, p1
对象的 isa
发生改变后,其执行的 setAge
也发生了改变。
我们知道, p2
在调用 setAge
方法时,首先会通过 p2
对象的 isa
指针找到 BAOPerson
类对象,然后在类对象中找到 setAge
方法,最终找到方法对应的实现。如下图所示:
但是, p1
对象的 isa
在添加 KVO 之后已经指向了 NSKVONotifying_BAOPerson
类对象, NSKVONotifying_BAOPerson
则是 BAOPerson
的子类。 NSKVONotifying_BAOPerson
是 runtime 在运行时生成的。因此, p1
对象在调用 setAge
方法时必然会根据 p1
的 isa
找到 NSKVONotifying_BAOPerson
,并在 NSKVONotifying_BAOPerson
中找到 setAge
方法及其实现。
经查阅资料了解到, NSKVONotifying_BAOPerson
中的 setAge
方法中其实调用了 Foundation 框架中 C 语言函数 _NSsetIntValueAndNotify
, _NSsetIntValueAndNotify
内部的操作大致是:首先调用 willChangeValueForKey
方法,然后调用父类的 setAge
方法对成员变量赋值,最后调用 didChangeValueForKey
方法。 didChangeValueForKey
方法中会调用监听者的监听方法,最终调用监听者的 observeValueForKeyPath
方法。
KVO 原理验证
前面我们已经通过断点打印 isa
指针的方式验证了: p1
对象在添加 KVO 后,其 isa
指针会指向一个通过 runtime 创建的 BAOPerson
的子类 NSKVONotifying_BAOPerson
。
下面我们可以通过打印方法实现的地址来看一下 p1
和 p2
的 setAge
方法实现的地址在添加 KVO 前后有什么变化。
// 通过methodForSelector找到方法实现的地址 NSLog(@"添加 KVO 之前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)], [p2 methodForSelector: @selector(setAge:)]); NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [p1 addObserver:self forKeyPath:@"age" options:options context:nil]; NSLog(@"添加 KVO 之后 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)], [p2 methodForSelector: @selector(setAge:)]);
执行上述代码,可以发现:在添加 KVO 之前, p1
和 p2
的 setAge
方法实现的地址是相同的;在添加 KVO 之后, p1
的 setAge
方法实现的地址发生了改变。通过打印方法实现可以证明, p1
的 setAge
方法的实现由 BAOPerson
类方法中的 setAge
方法转换成了 Foundation 框架中的 C 函数 _NSSetIntValueAndNotify
。
事实上,Foundation 框架中很多例如 _NSSetBoolValueAndNotify
、 _NSSetCharValueAndNotify
、 _NSSetFloatValueAndNotify
、 _NSSetLongValueAndNotify
等函数。
为了查看 Foundation 框架中的相关函数,我们找到 Foundation 文件,通过命令行查询:
nm Foundation | grep ValueAndNotify
中间类内部结构
NSKVONotifying_BAOPerson
作为 BAOPerson
的子类,其 superclass
指针指向 BAOPerson
类,其内部对 setAge
方法做了单独的实现,那么 NSKVONotifying_BAOPerson
同 BAOPerson
类的差别可能就在于其内存储的对象方法及实现不同。我们通过 runtime 分别打印 BAOPerson
类对象和 NSKVONotifying_BAOPerson
类对象内存储的对象方法。
- (void)viewDidLoad { [super viewDidLoad]; BAOPerson *p1 = [[BAOPerson alloc] init]; BAOPerson *p2 = [[BAOPerson alloc] init]; p1.age = 1; p1.age = 2; p2.age = 2; NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [p1 addObserver:self forKeyPath:@"age" options:options context:nil]; [self printMethods: object_getClass(p2)]; [self printMethods: object_getClass(p1)]; p1.age = 10; [p1 removeObserver:self forKeyPath:@"age"]; } - (void)printMethods:(Class)cls { unsigned int count; Method *methods = class_copyMethodList(cls, &count); NSMutableString *methodNames = [NSMutableString string]; [methodNames appendFormat:@"%@ - ", cls]; for (int i = 0 ; i < count; i++) { Method method = methods[i]; NSString *methodName = NSStringFromSelector(method_getName(method)); [methodNames appendString:methodName]; [methodNames appendString:@" "]; } NSLog(@"%@", methodNames); free(methods); }
上述代码的打印结果如下:
可以发现, NSKVONotifying_BAOPerson
中有 4 个对象方法,分别是:
setAge: class dealloc _isKVOA
NSKVONotifying_BAOPerson
重写 class
方法是为了隐藏 NSKVONotifying_BAOPerson
不被外界看到。我们在 p1
添加 KVO 之后,分别打印 p1
和 p2
对象的 class
,可以发现它们都返回 BAOPerson
。
NSLog(@"%@, %@", [p1 class], [p2 class]); // 打印结果 BAOPerson, BAOPerson
综上,我们可以画出 NSKVONotifying_BAOPerson
的内部结构及方法调用顺序。
验证 didChangeValueForKey:
内部调用 observeValueForKeyPath:ofObject:change:context:
方法
在 BAOPerson
类中重写 willChangeValueForKey:
和 didChangeValueForKey:
方法,模拟它们的实现。
- (void)setAge:(NSInteger)age { NSLog(@"override setAge"); _age = age; } - (void)willChangeValueForKey:(NSString *)key { NSLog(@"willChangeValueForKey: - begin"); [super willChangeValueForKey:key]; NSLog(@"willChangeValueForKey: - end"); } - (void)didChangeValueForKey:(NSString *)key { NSLog(@"didChangeValueForKey: - begin"); [super didChangeValueForKey:key]; NSLog(@"didChangeValueForKey: - end"); }
通过运行上述代码,可以确定是在 didChangeValueForKey:
方法内部调用了监听者的 observeValueForKeyPath:ofObject:change:context:
方法。
根据上述原理,可以通过调用 willChangeValueForKey:
和 didChangeValueForKey:
来手动触发 KVO。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。