KVO实现原理 KVO(key value observing)
键值监听是我们在开发中常使用的用于监听特定对象属性值变化的方法,常用于监听数据模型的变化 。 KVO是为了监听一个对象的某个属性值是否发生变化。在属性值发生变化的时候,肯定会调用其setter方法。所以KVO的本质就是监听对象有没有调用被监听属性对应的setter方法
。 在学习实现原理之前我们首先先了解一下KVO
常用的有哪些方法。
KVO常用方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 - (void )addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions )options context:(nullable void *)context; - (void )removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7 ), ios(5.0 ), watchos(2.0 ), tvos(9.0 )); - (void )removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; - (void )observeValueForKeyPath:(NSString *)keyPath ofObject:(id )object change:(NSDictionary <NSKeyValueChangeKey ,id > *)change context:(void *)context;
KVO简单实现 我们创建一个person
对象,然后在里面添加一个age
属性,我们就来观察一下age
属性。person对象
1 2 3 4 5 6 #import <Foundation/Foundation.h> @interface Person : NSObject @property (nonatomic ,assign ) NSInteger age;@end
简单实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #import "ViewController.h" #import "Person.h" @interface ViewController ()@property (nonatomic ,strong ) Person *p1;@property (nonatomic ,strong ) Person *p2;@end @implementation ViewController - (void )viewDidLoad { [super viewDidLoad]; self .p1 = [[Person alloc]init]; self .p2 = [[Person alloc]init]; self .p1.age = 10 ; self .p2.age = 20 ; NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld ; [self .p1 addObserver:self forKeyPath:@"age" options:options context:@"123" ]; } - (void )touchesBegan:(NSSet <UITouch *> *)touches withEvent:(UIEvent *)event{ self .p1.age = arc4random()%100 ; self .p2.age = arc4random()%100 ; } - (void )dealloc { [self .p1 removeObserver:self forKeyPath:@"age" context:@"123" ]; } - (void )observeValueForKeyPath:(NSString *)keyPath ofObject:(id )object change:(NSDictionary <NSKeyValueChangeKey ,id > *)change context:(void *)context { NSLog (@"监听到%@的%@属性值改变了 - %@ - %@" , object, keyPath, change, context); }
以上代码就是一个KVO
的简单实现,但是我们有没有想过他的内部到底是怎样实现的呢,今天我们就来探究一下KVO
的内部实现原理。
KVO的内部实现 探究一个对象底层实现最简单的办法就行打印一些对象信息,看看有什么改变,我们在给person1
添加监听之前分别打印p1,p2
的类信息。 代码实现:
1 2 3 4 5 6 7 NSLog (@"person1添加KVO监听之前 - %@ %@" , object_getClass(self .p1), object_getClass(self .p2));NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld ;[self .p1 addObserver:self forKeyPath:@"age" options:options context:@"123" ]; NSLog (@"person1添加KVO监听之后 - %@ %@" , object_getClass(self .p1), object_getClass(self .p2));
打印结果:
1 2 2020 -06 -23 12 :17 :41.812979 +0800 KVO[28865 :5328765 ] person1添加KVO监听之前 - Person Person2020 -06 -23 12 :17 :41.814729 +0800 KVO[28865 :5328765 ] person1添加KVO监听之后 - NSKVONotifying_Person Person
我们根据结果看到,在添加KVO观察者之后p1
的类对象由Person
变成了NSKVONotifying_Person
,虽然p1
的类对象变成了NSKVONotifying_Person
,但是我们在调用的时候感觉我们的p1
的类对象还是Person
,所以,我们可以猜测KVO
会在运行时动态创建一个新类,将对象的isa
指向新创建的类,新类是原类的子类
,命名规则是NSKVONotifying_xxx
的格式。KVO为了使其更像之前的类,还会将对象的class实例方法重写
,使其更像原类。
查看P1内部方法是否改变
我们在发现p1
的类对象由Person
变成了NSKVONotifying_Person
,那我们也随便打印一下Person
和NSKVONotifying_Person
内部方法都变成了什么。
打印一下方法名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 - (void )printMethodNamesOfClass:(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 appendString:methodName]; [methodNames appendString:@", " ]; } free(methodList); NSLog (@"%@ %@" , cls, methodNames); }
然后我们分别在KVO监听前后在分别打印一下p1
的类对象:
1 2 3 4 5 6 7 NSLog (@"person1添加KVO监听之前的内部方法===" );[self printMethodNamesOfClass:object_getClass(self .p1)]; NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld ;[self .p1 addObserver:self forKeyPath:@"age" options:options context:@"123" ]; NSLog (@"person1添加KVO监听之后的内部方法===" );[self printMethodNamesOfClass:object_getClass(self .p1)];
打印结果:
1 2 3 4 2020 -06 -23 12 :21 :35.457729 +0800 KVO[28868 :5329249 ] person1添加KVO监听之前的内部方法===2020 -06 -23 12 :21 :35.458094 +0800 KVO[28868 :5329249 ] Person setAge:, age,2020 -06 -23 12 :21 :35.459171 +0800 KVO[28868 :5329249 ] person1添加KVO监听之后的内部方法===2020 -06 -23 12 :21 :35.459334 +0800 KVO[28868 :5329249 ] NSKVONotifying_Person setAge:, class , dealloc, _isKVOA,
我们在来打印一些KVO监听前后setAge
方法发生了什么改变,因为值得改变肯定是因为set
方法导致的,所以我们打印一下setAge
方法。methodForSelector
可以打印方法地址,我们分别在KVO监听前后打印。
1 2 3 4 5 6 7 8 9 10 NSLog (@"person1添加KVO监听之前 - %p %p" ,[self .p1 methodForSelector:@selector (setAge:)], [self .p2 methodForSelector:@selector (setAge:)]); NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld ;[self .p1 addObserver:self forKeyPath:@"age" options:options context:@"123" ]; NSLog (@"person1添加KVO监听之后 - %p %p" ,[self .p1 methodForSelector:@selector (setAge:)], [self .p2 methodForSelector:@selector (setAge:)]);
打印结果:
1 2 2020 -06 -23 12 :25 :12.925158 +0800 KVO[28871 :5329672 ] person1添加KVO监听之前 - 0x100192338 0x100192338 2020 -06 -23 12 :25 :12.925468 +0800 KVO[28871 :5329672 ] person1添加KVO监听之后 - 0x2269f0a38 0x100192338
我们可以利用lldb分别看一下具体的方法实现:
1 2 3 4 5 6 7 2020 -06 -23 12 :25 :12.925158 +0800 KVO[28871 :5329672 ] person1添加KVO监听之前 - 0x100192338 0x100192338 2020 -06 -23 12 :25 :12.925468 +0800 KVO[28871 :5329672 ] person1添加KVO监听之后 - 0x2269f0a38 0x100192338 (lldb) p (IMP)0x100192338 (IMP) $0 = 0x0000000100192338 (KVO`-[Person setAge:] at Person.h:12 ) (lldb) p (IMP)0x2269f0a38 (IMP) $1 = 0x00000002269f0a38 (Foundation`_NSSetLongLongValueAndNotify) (lldb)
根据以上总结,我们大概猜到在使用KVO前后对象的改变了。未使用KVO监听的对象
使用KVO监听的对象
1、重写class方法是为了我们调用它的时候返回跟重写继承类之前同样的内容。KVO底层交换了 NSKVONotifying_Person 的 class 方法,让其返回 Person。
2、重写setter方法:在新的类中会重写对应的set方法,是为了在set方法中增加另外两个方法的调用。
1 2 - (void )willChangeValueForKey:(NSString *)key - (void )didChangeValueForKey:(NSString *)key
在didChangeValueForKey:方法再调用:
1 - (void )observeValueForKeyPath:(NSString *)keyPath ofObject:(id )object change:(NSDictionary *)change context:(void *)context
3、重写dealloc方法,销毁新生成的NSKVONotifying_类。
4、重写_isKVOA方法,这个私有方法估计可能是用来标示该类是一个 KVO 机制声称的类。
_NSSetLongLongValueAndNotify 在添加KVO监听方法以后setAge
方法变成了_NSSetLongLongValueAndNotify
,所以我们可以大概猜测动态监听方法主要就是在这里面实现的。我们可以在终端使用命令来查看NSSet * ValueAndNotify
的类型。
1 nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation | grep ValueAndNotify
我们可以在Person
类中重写willChangeValueForKey
和didChangeValueForKey
,来猜测一下_NSSetLongLongValueAndNotify
的内部实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 - (void )setAge:(NSInteger )age{ _age = age; NSLog (@"调用set方法" ); } - (void )willChangeValueForKey:(NSString *)key{ [super willChangeValueForKey:key]; NSLog (@"willChangeValueForKey" ); } - (void )didChangeValueForKey:(NSString *)key{ NSLog (@"didChangeValueForKey - begin" ); [super didChangeValueForKey:key]; NSLog (@"didChangeValueForKey - end" ); }
控制台打印的截图:
1 2 3 4 5 6 7 8 9 2020 -06 -24 22 :48 :59.344573 +0800 KVO[1762 :38744 ] willChangeValueForKey2020 -06 -24 22 :48 :59.344766 +0800 KVO[1762 :38744 ] 调用set方法2020 -06 -24 22 :48 :59.344915 +0800 KVO[1762 :38744 ] didChangeValueForKey - begin2020 -06 -24 22 :48 :59.345209 +0800 KVO[1762 :38744 ] 监听到<Person: 0x6000018a4250 >的age属性值改变了 - { kind = 1 ; new = 61 ; old = 10 ; } - 123 2020 -06 -24 22 :48 :59.345340 +0800 KVO[1762 :38744 ] didChangeValueForKey - end
根据打印结果我们可以推断_NSSetLongLongValueAndNotify
内部实现为: 1.调用willChangeValueForKey
方法。 2.调用setAge
方法。 3.调用didChangeValueForKey
方法。 4.在didChangeValueForKey
方法内部调用oberser
的observeValueForKeyPath: ofObject: change: context:
方法。
1 2 3 4 5 6 7 8 9 10 11 void _NSSetIntValueAndNotify() { [self willChangeValueForKey:@"age" ]; [super setAge:age]; [self didChangeValueForKey:@"age" ]; } - (void )didChangeValueForKey:(NSString *)key { [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil ]; }
面试题 讲了这些,我们来讨论面试题吧
1、iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
2、如何手动触发KVO方法 手动调用willChangeValueForKey
和didChangeValueForKey
方法。
键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey:
和 didChangeValueForKey
。在一个被观察属性发生改变之前, willChangeValueForKey:
一定会被调用,这就会记录旧的值。而当改变发生后, didChangeValueForKey
会被调用,继而observeValueForKey:ofObject:change:context:
也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了有人可能会问只调用didChangeValueForKey
方法可以触发KVO方法,其实是不能的,因为willChangeValueForKey:
记录旧的值,如果不记录旧的值,那就没有改变一说了。
3、直接修改成员变量会触发KVO吗 不会触发KVO,因为KVO的本质就是监听对象有没有调用被监听属性对应的setter方法
,直接修改成员变量,是在内存中修改的,不走set
方法。
4、不移除KVO监听,会发生什么
不移除会造成内存泄漏。
但是多次重复移除会崩溃。系统为了实现KVO,为NSObject添加了一个名为NSKeyValueObserverRegistration的Category,KVO的add和remove的实现都在里面。在移除的时候,系统会判断当前KVO的key是否已经被移除,如果已经被移除,则主动抛出一个NSException的异常。
参考demo