KVO实现原理

KVO(key value observing)键值监听是咱们在开发中常使用的用于监听特定对象属性值变化的方法,经常使用于监听数据模型的变化ios

KVO是为了监听一个对象的某个属性值是否发生变化。在属性值发生变化的时候,确定会调用其setter方法。因此KVO的本质就是监听对象有没有调用被监听属性对应的setter方法面试

在学习实现原理以前咱们首先先了解一下KVO经常使用的有哪些方法macos

KVO经常使用方法

/*
注册监听器
监听器对象为observer,被监听对象为消息的发送者即方法的调用者在回调函数中会被回传
监听的属性路径为keyPath支持点语法的嵌套
监听类型为options支持按位或来监听多个事件类型
监听上下文context主要用于在多个监听器对象监听相同keyPath时进行区分
添加监听器只会保留监听器对象的地址,不会增长引用,也不会在对象释放后置空,所以须要本身持有监听对象的强引用,该参数也会在回调函数中回传
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

/*
删除监听器
监听器对象为observer,被监听对象为消息的发送者即方法的调用者,应与addObserver方法匹配
监听的属性路径为keyPath,应与addObserver方法的keyPath匹配
监听上下文context,应与addObserver方法的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));

/*
与上一个方法相同,只是少了context参数
推荐使用上一个方法,该方法因为没有传递context可能会产生异常结果
*/
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

/*
监听器对象的监听回调方法
keyPath即为监听的属性路径
object为被监听的对象
change保存被监听的值产生的变化
context为监听上下文,由add方法回传
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
复制代码

KVO简单实现

咱们建立一个person对象,而后在里面添加一个age属性,咱们就来观察一下age属性 person对象数组

#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic,assign) NSInteger age;
@end
复制代码

简单实现bash

#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;

// 给person1对象添加KVO监听
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"];

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

以上代码就是一个KVO的简单实现,可是咱们有没有想过他的内部究竟是怎样实现的呢,今天咱们就来探究一下KVO的内部实现原理app

KVO的内部实现

探究一个对象底层实现最简单的办法就行打印一些对象信息,看看有什么改变dom

咱们在给person1添加监听以前分别打印p1,p2的类信息 代码实现函数

NSLog(@"person1添加KVO监听以前 - %@ %@",
object_getClass(self.p1),
object_getClass(self.p2));
// 给person1对象添加KVO监听
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));
复制代码

咱们根据结果看到,在添加KVO观察者以后p1的类对象由Person变成了NSKVONotifying_Person,虽然p1的类对象变成了NSKVONotifying_Person,可是咱们在调用的时候感受咱们的p1的类对象仍是Person,因此,咱们能够猜想KVO会在运行时动态建立一个新类,将对象的isa指向新建立的类,新类是原类的子类,命名规则是NSKVONotifying_xxx的格式。KVO为了使其更像以前的类,还会将对象的class实例方法重写,使其更像原类学习

查看P1内部方法是否改变ui

咱们在发现p1的类对象由Person变成了NSKVONotifying_Person,那咱们也随便打印一下Person和NSKVONotifying_Person内部方法都变成了什么

打印一下方法名

- (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的类对象

NSLog(@"person1添加KVO监听以前的内部方法===");
[self printMethodNamesOfClass:object_getClass(self.p1)];
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.p1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
NSLog(@"person1添加KVO监听以后的内部方法===");
[self printMethodNamesOfClass:object_getClass(self.p1)];
复制代码

打印结果

咱们在来打印一些KVO监听先后setAge方法发生了什么改变,由于值得改变确定是由于set方法致使的,因此咱们打印一下setAge方法。methodForSelector能够打印方法地址,咱们分别在KVO监听先后打印

NSLog(@"person1添加KVO监听以前 - %p %p",
[self.p1 methodForSelector:@selector(setAge:)],
[self.p2 methodForSelector:@selector(setAge:)]);

// 给person1对象添加KVO监听
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:)]);
复制代码

打印结果

2018-09-04 10:41:05.823343+0800 KVO[21971:1023542] person1添加KVO监听以前 - 0x103f18540 0x103f18540
2018-09-04 10:41:05.823702+0800 KVO[21971:1023542] person1添加KVO监听以后 - 0x10425ebf4 0x103f18540
复制代码

咱们能够利用lldb分别看一下具体的方法实现:

根据以上总结,咱们大概猜到在使用KVO先后对象的改变了 未使用KVO监听的对象

使用KVO监听的对象

  • 一、重写class方法是为了咱们调用它的时候返回跟重写继承类以前一样的内容。KVO底层交换了 NSKVONotifying_Person 的 class 方法,让其返回 Person
  • 二、重写setter方法:在新的类中会重写对应的set方法,是为了在set方法中增长另外两个方法的调用
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
复制代码

在didChangeValueForKey:方法再调用

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
复制代码
  • 三、重写dealloc方法,销毁新生成的NSKVONotifying_类。
  • 四、重写_isKVOA方法,这个私有方法估计多是用来标示该类是一个 KVO 机制声称的类。

_NSSetLongLongValueAndNotify

在添加KVO监听方法之后setAge方法变成了_NSSetLongLongValueAndNotify,因此咱们能够大概猜想动态监听方法主要就是在这里面实现的

咱们能够在终端使用nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation | grep ValueAndNotify命令来查看NSSet*ValueAndNotify的类型

咱们能够在Person类中重写willChangeValueForKey和didChangeValueForKey,来猜想一下_NSSetLongLongValueAndNotify的内部实现

- (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");
}
复制代码

根据打印结果咱们能够推断_NSSetLongLongValueAndNotify内部实现为

  • 一、调用willChangeValueForKey方法
  • 二、调用setAge方法
  • 三、调用'didChangeValueForKey'方法
  • 四、'didChangeValueForKey'方法内部调用oberser的observeValueForKeyPath:ofObject:change:context:方法
// 伪代码
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}

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

面试题

讲了这些,咱们来讨论面试题吧

一、iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

  • 一、利用RuntimeAPI动态生成一个子类NSKVONotifying_XXX,而且让instance对象的isa指向这个全新的子类NSKVONotifying_XXX
  • 二、当修改对象的属性时,会在子类NSKVONotifying_XXX调用Foundation的_NSSetXXXValueAndNotify函数
  • 三、在_NSSetXXXValueAndNotify函数中依次调用 - 一、willChangeValueForKey - 二、父类原来的setter - 三、didChangeValueForKey,didChangeValueForKey:内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)

二、如何手动触发KVO方法

手动调用willChangeValueForKey和didChangeValueForKey方法

键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangeValueForKey。在一个被观察属性发生改变以前, willChangeValueForKey: 必定会被调用,这就 会记录旧的值。而当改变发生后, didChangeValueForKey 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。若是能够手动实现这些调用,就能够实现“手动触发”了

有人可能会问只调用didChangeValueForKey方法能够触发KVO方法,实际上是不能的,由于willChangeValueForKey: 记录旧的值,若是不记录旧的值,那就没有改变一说了

三、直接修改为员变量会触发KVO吗

不会触发KVO,由于KVO的本质就是监听对象有没有调用被监听属性对应的setter方法,直接修改为员变量,是在内存中修改的,不走set方法

四、不移除KVO监听,会发生什么

  • 不移除会形成内存泄漏
  • 可是屡次重复移除会崩溃。系统为了实现KVO,为NSObject添加了一个名为NSKeyValueObserverRegistration的Category,KVO的add和remove的实现都在里面。在移除的时候,系统会判断当前KVO的key是否已经被移除,若是已经被移除,则主动抛出一个NSException的异常

原文地址