欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

iOS-详解KVO底层实现

程序员文章站 2022-04-13 12:21:00
...

前言

KVO: Key-Value-Observer,它来源于观察者模式, 其基本思想(copy于某度)是
一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。

本质

当某个类的实例对象的key第一次被观察时,系统就会在运行期动态地创建该类的一个派生类NSKVONotifying_类名,在这个派生类中重写该类中被观察的属性的 setter 方法。

一步一步验证

  • ①. KVO的本质就是监听对象的属性进行赋值的时候有没有调用setter方法. 如果有调用setter方法, 就会接收到属性变更的通知, 反之则没有.

我们新建一个类Person, 定义一个属性name, 并自己实现它的setter方法

iOS-详解KVO底层实现
1 @property(nonatomic, copy) NSString *name;
2 
3 - (void)setName:(NSString *)name
4 {
5     _name = [name copy];
6 
7     NSLog(@"%s", __FUNCTION__);
8 }
iOS-详解KVO底层实现

随后, 我们在ViewController中添加Person的实例对象的属性name的属性监听, 暨添加KVO, 并实现处理变更通知方法 observeValueForKeyPath

1   self.p = [[Person alloc] init];
2 [self.p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil]; 

 

1 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
2 {
3     NSLog(@"%@, %@", keyPath, self.p.name);
4 }

 

此时, 我们在点击屏幕的时候, 给实例对象p的name进行赋值

1 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
2     static int i = 0;
3     self.p.name = [NSString stringWithFormat:@"name->%d", i++];
4 }

运行程序, 点击两次屏幕, 我们可以可以观察到打印如下

1 -[Person setName:]
2 name, name->0
3 
4  -[Person setName:]
5 name, name->1

我们再进行一次反证, 我们不使用setter方法对name进行赋值, 看是否属性的值改变会被监听
我们将Person.h修改如下

iOS-详解KVO底层实现
1 @interface Person : NSObject
2 {
3 @public NSString *_name;
4 }
5 @property(nonatomic, copy) NSString *name;
6 @end
iOS-详解KVO底层实现

此时, @property只会帮我们生成getter方法(setter已经被我们自己实现了的), 不会生产_name成员变量

我们修改点击屏幕的代码如下

1 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
2     static int i = 0;
3     self.p->_name = [NSString stringWithFormat:@"name->%d", i++];
4 }

此时我们运行程序会发现, 不管怎么点击屏幕, 控制台都不会有任何打印.

结论:
 KVO的本质就是监听对象的属性进行赋值的时候有没有调用`setter`方法

 

② 当某个类的实例对象的key第一次被观察时,系统就会在运行期动态地创建该类的一个派生类NSKVONotifying_类名,在这个派生类中重写该类中被观察的属性的 setter 方法。

如果我们要重写方法, 一般都会调用[super 方法名], 那么系统是怎么做到重写属性的setter方法, 且通知观察者Observer的呢? 下面我们一步一步来进行分析:

iOS-详解KVO底层实现
ViewController.m

如图所示, 我们有断点A和B, 此时我们运行程序, 程序在停留在断点A处, 我们观察isa指针

iOS-详解KVO底层实现
isa

此时,isa指针指向Person类
我们跳过这个断点, continue, 并点击屏幕, 此时程序停留到了断点B, 此时我们再次观察isa

iOS-详解KVO底层实现
isa

此时isa指针被系统动态的指向了派生类NSKVONotifying_Person. 由此, 结论被证.

自己实现KVO

由于系统是自动实现的派生类NSKVONotifying_Person, 这儿我们自己手动创建一个派生类ALINKVONotifying_Person, 集成自Person. 同时给NSObject创建一个分类, 让每一个对象都拥有我们自定义的KVO特性.

NSObject+KVO.h

1
2
3
4
5
#import <Foundation/Foundation.h>
 
@interface NSObject (KVO)
- (void)alin_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end

NSObject+KVO.m

 

iOS-详解KVO底层实现
 1 #import "NSObject+KVO.h"
 2 #import "ALINKVONotifying_Person.h"
 3 #import <objc/message.h>
 4 
 5 NSString *const ObserverKey = @"ObserverKey";
 6 
 7 @implementation NSObject (KVO)
 8 
 9 // 仿系统的, 前缀是为了区别系统的
10 - (void)alin_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
11 {
12     // 把观察者保存到当前对象
13     objc_setAssociatedObject(self, (__bridge const void *)(ObserverKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
14 
15     // 修改对象isa指针
16     object_setClass(self, [ALINKVONotifying_Person class]);
17 }
18 @end
iOS-详解KVO底层实现

ALINKVONotifying_Person.m

iOS-详解KVO底层实现
 1 #import "ALINKVONotifying_Person.h"
 2 #import <objc/runtime.h>
 3 
 4 extern NSString *const ObserverKey;
 5 
 6 @implementation ALINKVONotifying_Person
 7 - (void)setName:(NSString *)name
 8 {
 9     [super setName:name];
10 
11     // 获取观察者
12     id obsetver = objc_getAssociatedObject(self, ObserverKey);
13 
14     [obsetver observeValueForKeyPath:@"name" ofObject:self change:nil context:nil];
15 }
16 @end
iOS-详解KVO底层实现

此时我们调用自己定义的监听方法, 效果和系统的也是一样的

  1 [self.p alin_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil]; 


文/Monkey_ALin(简书作者)

原文链接:http://www.jianshu.com/p/51c13fdec907
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
 
 

三、实现原理

1、KVC如何访问属性值

KVC再某种程度上提供了访问器的替代方案。不过访问器方法是一个很好的东西,以至于只要是有可能,KVC也尽量再访问器方法的帮助下工作。为了设置或者返回对象属性,KVC按顺序使用如下技术:
①检查是否存在-<key>、-is<key>(只针对布尔值有效)或者-get<key>的访问器方法,如果有可能,就是用这些方法返回值;
检查是否存在名为-set<key>:的方法,并使用它做设置值。对于-get<key>和-set<key>:方法,将大写Key字符串的第一个字母,并与Cocoa的方法命名保持一致;
②如果上述方法不可用,则检查名为-_<key>、-_is<key>(只针对布尔值有效)、-_get<key>和-_set<key>:方法;
③如果没有找到访问器方法,可以尝试直接访问实例变量。实例变量可以是名为:<key>或_<key>;
④如果仍为找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。

2、KVC/KVO实现原理

键值编码和键值观察是根据isa-swizzling技术来实现的,主要依据runtime的强大动态能力。下面的这段话是引自网上的一篇文章:
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。
派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。
同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而**键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。
原文写的很好,还举了解释性的例子,大家可以去看看。
在我之前的一篇介绍Objective-C类和元类的文章:
中介绍过,isa指针指向的其实是类的元类,如果之前的类名为:Person,那么被runtime更改以后的类名会变成:NSKVONotifying_Person。
新的NSKVONotifying_Person类会重写以下方法:
增加了监听的属性对应的set方法,class,dealloc,_isKVOA。
①class
重写class方法是为了我们调用它的时候返回跟重写继承类之前同样的内容。
打印如下内容:
[java] view plain copy
  1. NSLog(@"self->isa:%@",self->isa);  
  2. NSLog(@"self class:%@",[self class]);  
在建立KVO监听前,打印结果为:
[java] view plain copy
  1. self->isa:Person  
  2. self class:Person  
在建立KVO监听之后,打印结果为:
[java] view plain copy
  1. self->isa:NSKVONotifying_Person  
  2. self class:Person  
这也是isa指针和class方法的一个区别,大家使用的时候注意。
②重写set方法
新类会重写对应的set方法,是为了在set方法中增加另外两个方法的调用:
[java] view plain copy
  1. - (void)willChangeValueForKey:(NSString *)key  
  2. - (void)didChangeValueForKey:(NSString *)key  
其中,didChangeValueForKey:方法负责调用:
[java] view plain copy
  1. - (void)observeValueForKeyPath:(NSString *)keyPath  
  2.                       ofObject:(id)object  
  3.                         change:(NSDictionary *)change  
  4.                        context:(void *)context  
方法,这就是KVO实现的原理了!
如果没有任何的访问器方法,-setValue:forKey方法会直接调用:
[java] view plain copy
  1. - (void)willChangeValueForKey:(NSString *)key  
  2. - (void)didChangeValueForKey:(NSString *)key  
如果在没有使用键值编码且没有使用适当命名的访问起方法的时候,我们只需要显示调用上述两个方法,同样可以使用KVO!
总结一下,想使用KVO有三种方法:
1)使用了KVC
使用了KVC,如果有访问器方法,则运行时会在访问器方法中调用will/didChangeValueForKey:方法;
没用访问器方法,运行时会在setValue:forKey方法中调用will/didChangeValueForKey:方法。
2)有访问器方法
运行时会重写访问器方法调用will/didChangeValueForKey:方法。
因此,直接调用访问器方法改变属性值时,KVO也能监听到。
3)显示调用will/didChangeValueForKey:方法。
总之,想使用KVO,只要有will/didChangeValueForKey:方法就可以了。
③_isKVOA
这个私有方法估计是用来标示该类是一个 KVO 机制声称的类。
相关标签: KVO