KVO实现原理 KVO(key value observing)键值监听是我们在开发中常使用的用于监听特定对象属性值变化的方法,常用于监听数据模型的变化 。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内部实现为:willChangeValueForKey方法。setAge方法。didChangeValueForKey方法。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的本质就是监听对象有没有调用被监听属性对应的setter方法,直接修改成员变量,是在内存中修改的,不走set方法。
4、不移除KVO监听,会发生什么 
不移除会造成内存泄漏。 
但是多次重复移除会崩溃。系统为了实现KVO,为NSObject添加了一个名为NSKeyValueObserverRegistration的Category,KVO的add和remove的实现都在里面。在移除的时候,系统会判断当前KVO的key是否已经被移除,如果已经被移除,则主动抛出一个NSException的异常。 
 
参考demo