KVO 底层实现原理

19 篇文章 0 订阅

一 、 基本使用

概述

KVO全称NSKeyValueObserving,是一个非正式协议,它定义了对象之间观察和通知状态改变的通用机制的。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。

在 Objective-C 和 Cocoa 中,有许多事件之间进行通信的方式,并且每个都有不同程度的形式和耦合

  • NSNotification & NSNotificationCenter 提供了一个中央枢纽,一个应用的任何部分都可能通知或者被通知应用的其他部分的变化。唯一需要做的是要知道在寻找什么,主要是通知的名字。例如,UIApplicationDidReceiveMemoryWarningNotification 是给应用发了一个内存不足的信号。

  • Key-Value Observing 允许 ad-hoc,通过在特定对象之间监听一个特定的 keypath 的改变进行事件内省。例如:一个 ProgressView 可以观察 网络请求的 numberOfBytesRead 来更新它自己的 progress 属性。

  • Delegate 是一个流行的传递事件的设计模式,通过定义一系列的方法来传递给指定的处理对象。例如:UIScrollView 每次它的 scroll offset 改变的时候都会发送 scrollViewDidScroll: 到它的代理

  • Callbacks 不管是像 NSOperation 里的 completionBlock(当 isFinished==YES 的时候会触发),还是 C 里边的函数指针,传递一个函数钩子比如 SCNetworkReachabilitySetCallback(3)。

KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而不一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。

下面编写一段代码感受一下

//
//  KVOPerson.h
//  KVODemo
//
//  Created by tinghou on 2018/11/14.
//  Copyright © 2018年 tinghhout. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface KVOPerson : NSObject
@property (assign, nonatomic) int age;
@property (assign, nonatomic) int height;
@end

添加监听

 self.person1 = [[KVOPerson alloc] init];
    self.person1.age = 1;
    self.person1.height = 11;
    
    self.person2 = [[KVOPerson alloc] init];
    self.person2.age = 2;
    self.person2.height = 22;
    
    // 给对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    [self.person1 addObserver:self forKeyPath:@"height" options:options context:@"456"];

改变值

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person1.age = 2;
    self.person2.age = 12;
    
    self.person1.height = 3;
    self.person2.height = 23;
}

观察改变

// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

运行程序,我们就可以看到结果了,监听到了新值和旧值
在这里插入图片描述

总结

使用KVO分为三个步骤:

  • 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。
  • 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
  • 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash。

二 、 本质分析

上面代码创建两个Person对象person1和person2,监听person1的age属性而不监听person2,然后改变person1和person2的age属性。
现在我们打印一下person类,当添加kvo监听之后打印两个对象

 NSLog(@"person1添加KVO监听之后:-%@ %@", object_getClass(self.person1), object_getClass(self.person2));

然后打印的结果是这样的:

2018-11-16 20:40:09.282241+0800 KVODemo[32712:1011869] libMobileGestalt MobileGestalt.c:890: MGIsDeviceOneOfType is not supported on this platform.
2018-11-16 20:40:43.134227+0800 KVODemo[32712:1011869] person1添加KVO监听之后:-NSKVONotifying_KVOPerson KVOPerson

或者我们打印person对象的isa指针

(lldb) po self.person1.isa
NSKVONotifying_KVOPerson

  Fix-it applied, fixed expression was: 
    self.person1->isa
(lldb) po self.person2.isa
KVOPerson

  Fix-it applied, fixed expression was: 
    self.person2->isa
(lldb) 

发现上面person1添加了kvo监听之后产生了一个 NSKVONotifying_KVOPerson 类,而person2没有添加kvo监听就是原类。现在我们在person类里面添加这些代码

//
//  KVOPerson.m
//  KVODemo
//
//  Created by tinghou on 2018/11/14.
//  Copyright © 2018年 tinghhout. All rights reserved.
//

#import "KVOPerson.h"

@implementation KVOPerson
- (void)setAge:(int)age
{
    _age = age;
    
    NSLog(@"setAge:");
}

//- (int)age
//{
//    return _age;
//}

- (void)willChangeValueForKey:(NSString *)key
{
    [super willChangeValueForKey:key];
    
    NSLog(@"willChangeValueForKey");
}

- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"didChangeValueForKey - begin");
    
    [super didChangeValueForKey:key];
    
    NSLog(@"didChangeValueForKey - end");
}
@end

再次运行看到结果打印是这样的

2018-11-16 20:46:00.552063+0800 KVODemo[32822:1033907] willChangeValueForKey
2018-11-16 20:46:00.552197+0800 KVODemo[32822:1033907] setAge:
2018-11-16 20:46:00.552260+0800 KVODemo[32822:1033907] didChangeValueForKey - begin
2018-11-16 20:46:00.552416+0800 KVODemo[32822:1033907] 监听到<KVOPerson: 0x600000304a10>的age属性值改变了 - {
    kind = 1;
    new = 2;
    old = 1;
} - 123
2018-11-16 20:46:00.552488+0800 KVODemo[32822:1033907] didChangeValueForKey - end

通过上面的打印顺序我们看到:

  • 1.首先调用willChangeValueForKey:方法。
  • 2.然后调用setAge:方法真正的改变属性的值。
  • 3.开始调用didChangeValueForKey:这个方法,调用[super didChangeValueForKey:key]时会通知监听者属性值已经改变,然后监听者执行自己的- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context这个方法。

由此猜测 person类添加kvo之后动态产生的类NSKVONotifying_KVOPerson大致逻辑是这样的:
在NSKVONotifying_KVOPerson这个子类的setAge:方法中主要是实现了一个C方法_NSSetIntValueAndNotify(),这个方法的实现分三步,首先是属性将要改变时调用willChangeValueForKey:,然后是调用父类即Person类的setAge:方法来真正的改变age属性的值,当age属性的值改变完成之后再调用didChangeValueForKey:这个方法来通知监听者属性值已经改变。下面是模拟它的伪代码:

- (void)setAge:(int)age
{
    _NSSetIntValueAndNotify();
}

// 伪代码
void _NSSetIntValueAndNotify()
{
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key
{
    // 通知监听器,某某属性值发生了改变
    [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}

现在查看添加监听前后setAge:方法的实现来验证

  • (IMP)methodForSelector:(SEL)aSelector;这个方法是传入一个selector返回一个方法的实现即imp,这里我们打印一下person1添加监听前后person1和person2的setAge:方法的实现的地址来判断这两个对象调用的的setAge:方法是否发生了改变:
NSLog(@"person1添加监听之前:- %p %p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);
    
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"测试信息"];
    
NSLog(@"person1添加监听之后:- %p %p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);

打印结果:

person1添加监听之前:- 0x10f5c84d0 0x10f5c84d0
person1添加监听之后:- 0x10f96df8e 0x10f5c84d0

然后我们使用LLDB打印一下0x10f5c84d0和0x10f96df8e这两个地址的IMP,我们把地址强制转化为IMP然后转化出来:

(lldb) p (IMP)0x10678a4d0
(IMP) $0 = 0x000000010678a4d0 (KVODemo`-[KVOPerson setAge:] at Person.m:13)
(lldb) p (IMP)0x106b2ff8e
(IMP) $1 = 0x0000000106b2ff8e (Foundation`_NSSetIntValueAndNotify)

这样我们就看的很清晰了。
0x10678a4d0这个地址的setAge:实现是调用KVOPerson类的setAge:方法,并且是在KVOPerson.m的第13行。
而0x106b2ff8e这个地址的setAge:实现是调用_NSSetIntValueAndNotify这样一个C函数。
所以person2则没有发生变化,它一直是调用KVOPerson类的setAge:方法。而person1添加监听前后person1的setAge:方法发生了变化,添加监听前它是调用的KVOPerson类的setAge:方法,添加监听后变成了调用_NSSetIntValueAndNotify这样一个C函数。

由此得出KVO的本质是这样的:

  • 利用RuntimeAPI动态生成一个子类叫做 NSKVONotifying_KVOPerson,然后KVO会在这个派生类中,重写基类中任何被观察属性的setter方法,在setter方法中实现真正的通知机制,并且让instance对象的isa指向这个全新的子类
  • 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数,进而触发
    willChangeValueForKey:
    父类原来的setter
    didChangeValueForKey:
  • 内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)
- (void)setAge:(int)age

{

[super setAge:age];

[监听者 observeValueForKeyPath:@"age"  ofObject:self  change:@{}  context:nil];

}

由此还可以手动实现键值观察:

/**
 首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了; 
其次,要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。
 */
 // for manual KVO - age 手动实现键值观察
 - (int) age
 {
 return age;
 }
 
 - (void) setAge:(int)theAge
 {
 [self willChangeValueForKey:@"age"];
 age = theAge;
 [self didChangeValueForKey:@"age"];
 }
 
 + (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
 if ([key isEqualToString:@"age"]) {
 return NO;
 }
 
 return [super automaticallyNotifiesObserversForKey:key];
 }
 
 

在这里插入图片描述
由此得出使用了KVO监听的person对象是这样的:
在这里插入图片描述

未用了KVO监听的person对象是这样的:
在这里插入图片描述

参考文章 Key-Value Observing

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值