iOS-KVO 实现原理

一、概述

KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则其观察者就会接受到通知。简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了。
KVO其实也是“观察者”设计模式的一种应用。这种模式有利于两个类间的解耦合,尤其是对于 业务逻辑与视图控制 这两个功能的解耦合。

二、KVO 的具体实现

具体实现代码如下:

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()
/** p1 */
@property (strong, nonatomic) Person *p1;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 1.什么是通知
    
    // 3个对象
    self.p1 = [[Person alloc] init];
    self.p1.name = @"p1";
    //打印监听前类信息
    [p1 printInfo];
    // KVO是监听对象的属性值的改变的
    [self.p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    self.p1.name = @"123";
    //打印监听后类信息
     [p1 printInfo];
     [p1 removeObserver:self forKeyPath:@"name"];
     //打印移除监听后类信息
     [p1 printInfo];
}

// 这个方法时属于 NSObject 类的,任何对象都可以作为观察者
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    NSLog(@"监听到了%@的%@属性发生了改变", object, keyPath);
    NSLog(@"%@", change);
}

@end

person类方法:

-(void)printInfo
{
    NSLog(@"isa:%@, supper class:%@", NSStringFromClass(object_getClass(self)),
          class_getSuperclass(object_getClass(self)));
    
    NSLog(@"self:%@, [self superclass]:%@", self, [self superclass]);
    
    NSLog(@"age setter function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(setAge:)));
    
    NSLog(@"name setter function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(setName:)));
    NSLog(@"printInfo function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(printInfo)));
}

打印结果:

2014-05-11 19:55:34.319 KVO和KVC和通知代理[559:109378] 监听到了<Person: 0x15ee2baa0>的name属性发生了改变
2014-05-11 19:55:34.320 KVO和KVC和通知代理[559:109378] 123

printInfo各种情况下打印信息:

添加监听前:

isa:person, supper class:NSObject
self:<person: 0x17000d3c0>, [self superclass]:NSObject
age setter function pointer:0x1883c3400
name setter function pointer:0x1883c3400
printInfo function pointer:0x10008efcc

监听后:

isa:NSKVONotifying_person, supper class:person
self:<person: 0x17000d3c0>, [self superclass]:NSObject
age setter function pointer:0x1883c3400
name setter function pointer:0x1883c3400
printInfo function pointer:0x10008efcc

移除监听后:

isa:person, supper class:NSObject
self:<person: 0x17000d3c0>, [self superclass]:NSObject
age setter function pointer:0x1883c3400
name setter function pointer:0x1883c3400
printInfo function pointer:0x10008efcc

三、KVO 的实现原理

KVO 是基于运行时实现的 isa Class NSKVONotifying_Person
基本的原理:当观察某对象A时,KVO机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性keyPath的setter 方法。setter 方法随后负责通知观察对象属性的改变状况。

深入剖析:

Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为: NSKVONotifying_A的新类,该类继承自对象A的本类,且KVO为NSKVONotifying_A重写观察属性的setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。(备注: isa 混写(isa-swizzling)isa:is a kind of ; swizzling:混合,搅合;)
①NSKVONotifying_A类剖析:在这个过程,被观察对象的 isa 指针从指向原来的A类,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_A类,来实现当前类属性值改变的监听;
所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类(),就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A的中间类,并指向这个中间类了。(isa 指针的作用:每个对象都有isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。) 因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。
—>我猜,这也是KVO回调机制,为什么都俗称KVO技术为黑魔法的原因之一吧:内部神秘、外观简洁。
②子类setter方法剖析:KVO的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用2个方法:被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;之后observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
KVO为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
上述例子中,当 p1.name 的值改变时,p1对象的 isa 指针会指向 NSKVONotifying_Person,意味着,在程序运行时,会动态生成一个 NSKVONotifying_Person 类,该类继承于 Person,而且该类中也有个 -setName: 方法,方法中在设置 name 的同时实现了:

- (void)setName:(NSString *)name
{
    [super setName:name];
    
    // 这两个方法底层会调用observer的- (void)observeValueForKeyPath: ofObject: change: context:这个方法
    [self willChangeValueForKey:@"age"];
    [self didChangeValueForKey:@"age"];
}

五、特点:

观察者观察的是属性,只有遵循 KVO 变更属性值的方式才会执行KVO的回调方法,例如是否执行了setter方法、或者是否使用了KVC赋值。
如果赋值没有通过setter方法或者KVC,而是直接修改属性对应的成员变量,例如:仅调用_name = @"newName",这时是不会触发kvo机制,更加不会调用回调方法的。
所以使用KVO机制的前提是遵循 KVO 的属性设置方式来变更属性值。

六、步骤

1.注册观察者,实施监听;
2.在回调方法中处理属性发生的变化;
3.移除观察者

七、拓展

1.KVC与KVO的不同?
KVC(键值编码),即Key-Value Coding,一个非正式的Protocol,使用字符串(键)访问一个对象实例变量的机制。而不是通过调用Setter、Getter方法等显式的存取方式去问。
KVO(键值监听),即Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,对象就会接受到通知,前提是执行了setter方法、或者使用了KVC赋值。

2.和notification(通知)的区别?
notification比KVO多了发送通知的一步。两者都是一对多,但是对象之间直接的交互,notification明显得多,需要notificationCenter来做为中间交互。而KVO如我们介绍的,设置观察者->处理属性变化,至于中间通知这一环,则隐秘多了,只留一句“交由系统通知”,具体的可参照以上实现过程的剖析。
notification的优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,例如键盘、前后台等系统通知的使用也更显灵活方便。(参照通知机制第五节系统通知名称内容)

3.与delegate的不同?
和delegate一样,KVO和NSNotification的作用都是类与类之间的通信。但是与delegate不同的是:
这两个都是负责发送接收通知,剩下的事情由系统处理,所以不用返回值;而delegate 则需要通信的对象通过变量(代理)联系;
delegate一般是一对一,而这两个可以一对多。

4.涉及技术:
KVC/KVO实现的根本是Objective-C的动态性和runtime,以及访问器方法的实现;

总结:

对比其他的回调方式,KVO机制的运用的实现,更多的由系统支持,相比notification、delegate等更简洁些,并且能够提供观察属性的最新值以及原始值;但是相应的在创建子类、重写方法等等方面的内存消耗是很巨大的。所以对于两个类之间的通信,我们可以根据实际开发的环境采用不同的方法,使得开发的项目更加简洁实用。

另外需要注意的是,由于这种继承方式的注入是在运行时而不是编译时实现的,如果给定的实例没有观察者,那么KVO不会有任何开销,因为此时根本就没有KVO代码存在。但是即使没有观察者,委托和NSNotification还是得工作,这也是KVO此处零开销观察的优势。

异步:监听通知 主线程:发出通知 接收通知代码在主线程
主线程:监听通知 异步:发出通知 接收通知代码在异步
注意:在接收通知代码中 可以加上主队列任务
总结:接收通知代码 由 发出通知线程决定, KVO也一样

推荐阅读更多精彩内容