OC底层原理23:KVO底层原理分析

张建 lol

前言

KVO 全称 Key Value Observing,中文名为 键值观察,KVO是一种机制,它 允许将其他对象的指定属性的更改通知给对象

key Value Observing Programming Guide 官方文档中,有这么一句话:理解KVO之前,必须先理解 KVC(即KVO是基于KVC基础之上)

1
2
3
In order to understand key-value observing, you must first understand key-value coding.

KVC是键值编码,在对象创建完成后,可以动态的给对象属性赋值,而KVO是键值观察,提供了一种监听机制,当指定的对象的属性被修改后,则对象会收到通知,所以可以看出KVO是基于KVC的基础上对属性动态变化的监听

在iOS日常开发中,经常 使用KVO来监听对象属性的变化,并及时作出响应,即当指定的被观察对象属性被修改后,KVO 会自动通知响应的管擦着,那么 KVONSNotificationCenter 有什么区别呢?

  • 相同点

    • 两者的实现原理 都是观察者模式,都是用于 监听
    • 都能 实现一对多 的操作
  • 不同点

    • KVO只能用于监听对象属性的变化,并且属性名都是通过 NSString 来查找,编译器不会帮你检测对错和补全,纯手敲会比较容易出错

    • NSNotification发送监听(post) 的操作 我们可以控制KVO由系统控制

    • KVO 可以 记录新旧值变化

KVO 使用

基本使用

KVO的基本使用主要分为三步:

  • 注册观察者 addObserver:forKeyPath:option:context
1
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
  • 实现KVO回调 observerValueForKeyPath:ofObject:change:context
1
2
3
4
5
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"%@",change);
}
}
  • 移除观察者 removeObserver:forKeyPath:context
1
[self.person removeObserver:self forKeyPath:@"name" context:NULL];

context 使用

在官方文档中,针对 参数context 有如下说明:

大致含义就是:addObserver:forKeyPath:option:context: 方法中的 上下文context 指针包含任意数据,这些数据将在响应的更改通知中传递回观察者。可以通过 指定context为NULL,从而 依靠keyPath键路径字符串 传来的确定更改通知的来源,但是这种方法可能导致对象的父类由于不同的原因也观察到相同的键路径而导致问题。所以可以为每个观察到的 keyPath 创建一个不同的 context,从而 完全不需要进行字符串比较,从而可以更有效地进行通知解析

通俗的讲,context上下文 主要是用于 区分不同对象的同名属性,从而在KVO回调方法中可以 直接使用context进行区分,可以大大提高性能,以及代码的可读性,并且更加安全

context使用总结

  • 不使用 context,使用 keyPath 区分通知来源
1
2
// context的类型是 nullable void *,应该是NULL,而不是nil
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
  • 使用 context 区分通知来源

Person类中增加两个属性

1
2
@property (nonatomic,copy)NSString * name;
@property (nonatomic,copy)NSString * nick;

DetailViewController类中代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义context
static void * PersonNickContext = &PersonNickContext;
static void * PersonNameContext = &PersonNameContext;

// 注册观察者
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];

// KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNickContext) {
NSLog(@"%@",change);
}else if (context == PersonNameContext){
NSLog(@"%@",change);
}
}

// KVO移除
[self.person removeObserver:self forKeyPath:@"name"];
[self.person removeObserver:self forKeyPath:@"nick"];

移除KVO通知的必要性

在官方文档中,针对 KVO移除 有以下几点说明:

删除观察者时,请记住以下几点:

  • 要求被移除为观察者(如果尚未注册为观察者)会导致 NSRangeException,您可以对 removeObserver:forKeyPath:context: 进行一次调用,以对应对 addObserver:forKeyPath:option:context: 的调用,或者如果在您的应用中不可行,则将 removeObserver:forKeyPath:context: 调用在 try/catch块 内处理潜在的异常

  • 释放后,观察者不会自动将其自身移除。被观察对象继续发送通知,而或略了观察者的状态。但是,与发送到已释放对象的任何其他消息一样,更改通知会触发内存访问异常。因此,您 可以确保观察者在从内存中消失之前将自己移除

  • 该协议无法询问对象是观察者还是被观察者。构造代码以避免发布相关的错误。一种典型的模式是在观察者初始化期间(例如,在init或viewDidLoad中注册为观察者),并在释放过程中(通常 在dealloc中注销),以确保成对和有序添加和删除观察者,并确保观察者在注册之前被取消注册,从内存中释放出来

所以,总的来说,KVO注册观察者和移除观察者时需要成对出现的,如果 只注册不移除会出现类似野指针的崩溃(EXC_BAD_ACCESS),如下图所示:

崩溃的原因是,由于 第一次注册KVO观察者没有移除,再次进入页面,会 第二次注册KVO观察者,导致 KVO观察者的重复注册,而且第一次的通知对象还在内存中,没有进行释放,此时接收到属性值变化的通知,会出现 找不到原有的通知对象,只能找到现有的通知对象,即第二次KVO注册的观察者,所以 导致了类似野指针的崩溃,即一直保持着一个 野通知,且一直在监听

注:这里的崩溃案例是通过 单例对象 实现(崩溃有很大的几率,不是每次必现),因为单例对象在内存是常驻的,针对一般的类对象,貌似不移除也是可以的,但是为了防止线上意外,建议还是移除比较好

KVO的自动触发与手动触发

KVO观察者的 开启和关闭 有两种方式:自动和手动

  • 自动开关,返回 NO 就是监听不到,返回 YES 表示监听
1
2
3
4
// 自动开关 开启
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return YES;
}
  • 自动开关关闭的时候,可以通过 手动开关监听
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 自动开关 关闭
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
// 自动关闭时,手动开启
- (void)setName:(NSString *)name{
// 手动开关
[self willChangeValueForKey:@"name"];
_name = name; // 父类的setter方法
[self didChangeValueForKey:@"name"];
}

// 查看打印结果
2022-03-09 10:27:56.006156+0800 KVO_Demo[4580:3263311] {
kind = 1;
new = "hello world";
}

使用手动开关的好处就是你想监听就监听,不想监听关闭即可,比自动触发更方便灵活

KVO观察:一对多

KVO观察中的 一对多,意思是通过 注册一个KVO观察者,可以 监听多个属性的变化

以下载进度为例,比如目前有一个需求,需要根据 总的下载量totalData当前下载量currentData 来计算 当前的下载进度downloadProcess,实现如下:

【合二为一观察法】:

  • 分别观察 总的下载量totalData当前下载量currentData 两个属性,当其中一个发生变化 计算当前下载量进度downloadProcess,实现 keyPathsForValueAffectingValueForKey 方法,将两个观察合为一个观察,即 观察当前下载进度currentProcess
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
@interface Person : NSObject
@property (nonatomic,copy)NSString * name;
@property (nonatomic,copy)NSString * nick;
@property (nonatomic,assign)double totalData;
@property (nonatomic,assign)double currentData;
@property (nonatomic,copy)NSString * downloadProcess; // 下载进度
@property (nonatomic,strong)NSMutableArray * mArr; // 可变数组
+ (instancetype)shareInstance;
@end

@implementation Person
+ (instancetype)shareInstance{
static Person * person = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
person = [[Person alloc] init];
});
return person;
}

#pragma mark -2.合二为一的观察法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProcess"]) {
NSArray * affectingKeys = @[@"totalData",@"currentData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}

@end
  • 注册KVO观察者,当 currentData改变 时,计算 当前下载进度downloadProcess
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 2.合二为一的观察法
[self.person addObserver:self forKeyPath:@"downloadProcess" options:NSKeyValueObservingOptionNew context:NULL];

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 2.合二为一的观察法
self.person.currentData += 1;
self.person.totalData = 100;
}

#pragma mark -KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
// 2.合二为一的观察法
Person * person = (Person *)object;
NSLog(@"%f",person.currentData/person.totalData);
}

#pragma mark -delloc
-(void)dealloc{
// 2.合二为一的观察法
[self.person removeObserver:self forKeyPath:@"downloadProcess"];
}

KVO观察可变数组

KVO基于KVC 基础 之上 的,所以 可变数组 如果 直接添加数据,是 不会调用setter方法 的,所有 对可变数组的KVO观察 下面这种方式 是不生效 的,即直接通过 [self.person.mArr addObject:@"1"]; 像数组添加元素,是 不会触发KVO 通知回调的,如下代码是不会通知回调的:

  • Person.h
1
2
3
@interface Person : NSObject
@property (nonatomic,strong)NSMutableArray * mArr; // 可变数组
@end
  • DetailViewController.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 可变数组
self.person.mArr = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"mArr" options:NSKeyValueObservingOptionNew context:NULL];

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 可变数组
[self.person.mArr addObject:@"1"];
}

#pragma mark -KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
// 可变数组
NSLog(@"%@",change);
}

-(void)dealloc{
// 可变数组
[self.person removeObserver:self forKeyPath:@"mArr"];
}

在 KVC官方文档中,针对 可变数组的集合 类型,有如下说明,即访问集合对象需要通过 mutableArrayValueForKey 方法,这样才能 将元素添加到可变数组中

因此,需要将上面 可变数组 添加元素的方法,改为如下:

1
2
3
4
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 可变数组
[[self.person mutableArrayValueForKey:@"mArr"] addObject:@"1"];
}

运行结果如下,可以看到,元素被添加到可变数组了

1
2
3
4
5
6
7
2022-03-09 10:11:24.697680+0800 KVO_Demo[4557:3255716] {
indexes = "<_NSCachedIndexSet: 0x2826d91c0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 2; // 插入
new = (
1
);
}

其中的 kind 表示 键值变化的类型,是一个枚举,主要有以下 4 种:

1
2
3
4
5
6
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1, // 设值
NSKeyValueChangeInsertion = 2, // 插入
NSKeyValueChangeRemoval = 3, // 移除
NSKeyValueChangeReplacement = 4, // 替换
};

一般的 属性集合 的KVO观察是由区别的,其 kind不同,以 属性name可变数组 为例

  • 属性kind 一般是 设值

  • 可变数组kind 一般是 插入

KVO监听方法可以在子线程吗?

可以的,将修改对象属性过程放在子线程内执行,在监听回调方法内获取当前线程同样为子线程。故KVO的响应和KVO观察的值变化是在一个线程上的。从子线程发出的,接收就在子线程;从主线程发的接收就在主线程。但是 刷新UI一定要在主线程

如何手动触发KVO?

手动 调用 willChangeValueForKey:didChangeValueForKey:

通过KVC修改属性会触发KVO么?如果修改成变量的值呢?

可以,KVO 的本质就是在 调用属性的set方法 才触发了 KVO,如果直接修改成员变量的值,就不会触发set方法,所以也不会触发KVO。

哪些情况下使用kvo会崩溃,怎么防护崩溃

  • KVO 添加次数和移除次数不匹配:

  • 被观察者提前被释放,被观察者在 dealloc 时仍然注册着 KVO,导致崩溃。 例如:被观察者是局部变量的情况(iOS 10 及之前会崩溃)。

  • 添加了观察者,但未实现 observeValueForKeyPath:ofObject:change:context: 方法,导致崩溃。

  • 添加或者移除时 keypath == nil,导致崩溃。

  • 防护是封装好,防止重复添加和删除,并dealloc前移除。

KVO的底层原理探索

官方文档说明

在KVO的官方文档使用指南中,有如下说明

  • KVO 是使用 isa-swizzling 的技术实现的

  • 顾名思义,isa指针指向维护分配表的对象的类。该分配表实质上包含指向该类实现的方法的指针以及其他数据

  • 当为对象的属性 注册观察者时,将 修改 观察者对象的 isa指针,指向中间类 而不是真实类。结果,isa 指针的值不一定反映实例的实际类

  • 你不应该依靠isa指针来确定类的成员变量,相反,你应该 使用class方法来确定对象实例的类

代码调式探索

KVO只对属性观察

ZJPerson 中有一个 成员变量name属性nick,分别注册KVO观察者,触发属性变化时,会有什么现象?

  • 创建 ZJPerson 类,声明 成员变量name属性nick
1
2
3
4
5
6
7
@interface ZJPerson : NSObject
{
@public
NSString * name;
}
@property (nonatomic,copy)NSString * nick;
@end
  • 分别为 成员变量name属性nick 注册观察者
1
2
3
4
5
6
7
8
9
10
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];

self.person = [[ZJPerson alloc] init];

[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];

}
  • KVO触发通知操作
1
2
3
4
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person->name = @"ZJ";
self.person.nick = @"XJ";
}
  • KVO接收通知操作
1
2
3
4
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}

运行结果如下

1
2
3
4
2022-03-09 14:33:59.443356+0800 002--KVO原理探讨[4675:3328750] {
kind = 1;
new = XJ;
}

结论:KVO对 成员变量name不观察,只对 属性name观察,属性和成员变量的区别在于 属性多了一个setter方法,而 KVO恰好观察的是setter方法

中间类

根据官方文档所述,在 注册KVO观察者后,观察者对象的isa指针指向会发生改变

  • 注册观察者之前: 实例对象personisa 指针指向 ZJPerson

  • 注册观察者之后:实例对象personisa 指针指向 NSKVONotifing_ZJPerson

综上所述,在注册观察者后,实例对象isa 指针 指向ZJPerson类 变为了 NSKVONotifying_ZJPerson中间类,即实例对象的 isa 指针指向发生了变化

判断中间类是否是 派生类 即 子类?

那么这个动态生成的中间类 NSKVONotifying_ZJPersonZJPerson 有什么关系?下面通过代码来验证

可以通过下面封装的方法,获取ZJPerson的相关类

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
// 添加KVO之前
[self printClasses:[ZJPerson class]];

// 添加观察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];

// 添加KVO之后
[self printClasses:[ZJPerson class]];

#pragma mark -遍历类以及子类
- (void)printClasses:(Class)cls{
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 其中包含给定对象
NSMutableArray * mArr = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class * classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i < count; i ++) {
if (cls == class_getSuperclass(classes[i])) {
[mArr addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArr);
}

打印结果如下:

从打印结果可以得出,NSKVONotifying_ZJPersonZJPerson 的子类

中间类中有什么?

可以通过下面的方法获取 NSKVONotifying_ZJPerson 类中的所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 遍历方法
[self printClassAllMethod:objc_getClass("NSKVONotifying_ZJPerson")];

#pragma mark -遍历方法
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
// 获取一个类中的所有实例方法
Method * methodList = class_copyMethodList(cls, &count);
for (int i = 0; i < count; i ++) {
// 获取单个方法
Method method = methodList[i];
// 方法编号
SEL sel = method_getName(method);
// 方法实现
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@ - %p",NSStringFromSelector(sel),imp);
}
free(methodList);
}

查看打印结果

从结果中可以看出有四个方法,分别是 setNick、class、dealloc、_isKVOA,这些方法是 继承 还是 重写

  • ZJStudent(继承自ZJPerson) 中重写 setNick 方法,获取 ZJStudent 类的所有方法

由打印结果可知,如果是继承那么 NSKVONotifying_ZJPerson 打印的方法应该与 ZJPerson 打印的结果一致

  • 获取 ZJPersonNSKVONotifying_ZJPerson 的方法列表进行对比

【结论】 综上所述,有如下结论:面试重点

  • NSKVONotifying_ZJPerson 中间类 重写父类ZJPersonsetNick 方法

  • NSKVONotifying_ZJPerson 中间类 重写基类NSObjectclass、dealloc、_isKVOA 方法

    • 其中 dealloc 是释放方法
    • _isKVOA 判断当前是否是 KVO类

dealloc中移除观察者后,isa指向谁,以及中间类是否会销毁?

  • 移除观察者前:实例对象的isa 指向仍是 NSKVONotifying_ZJPerson中间类

  • 移除观察者之后:实例对象的 isa指针 指向更改为 ZJPerson类

所以,在 移除KVO观察者后isa 的指向由 KSKVONotifying_ZJPerson 变成了 ZJPerson

那么中间类从创建后,到 dealloc 方法中移除观察者之后,KSKVONotifying_ZJPerson 是否还存在?

  • 在上一个界面打印 ZJPerson 的子类情况,用于判断中间类是否销毁

通过子类的打印结果可以看出,中间类一旦生成,没有移除,没有销毁,还在内存中,主要是考虑 重用 的想法,即 中间类注册到内存中,为了考虑后续的重用问题,所以中间类一直存在

总结 重点

综上所述,关于 中间类,有如下说明:

  • 实例对象isa 的指向 在注册KVO观察者之后,由 原有类 更改为 中间类

  • 中间类 重写了观察 属性setter方法、class、dealloc、_isKVOA 方法

  • dealloc 方法中,移除KVO观察者之后实例对象isa 指向由 中间类 更改为 原有类

  • 中间类 从创建后,就一直 存在内存中,不会被销毁

自定义KVO

自定义KVO的流程,只是在系统的基础上针对其部分做了一些优化处理。

  • 模拟系统
  • 实现 KVO自动销毁机制
  • 响应式和函数式 整合

在系统中,注册观察者和KVO响应属于 响应式编程,是分开写的,在自定义为了代码更好的协调,使用 block 的形式,将注册和回调的逻辑组合在一起,即采用 函数式变成 方式

那么如何实现自定义KVO呢?分为以下几个步骤:

  • 注册观察者
1
2
3
4
5
6
7
8
// 定义blcock
typedef void(^ZJKVOBlock)(id observer,NSString * keyPath,id oldValue,id newValue);

// 创建一个 NSObject+ZJKVO的分类,注册观察者
@interface NSObject (ZJKVO)
- (void)zj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(ZJKVOBlock)block;
- (void)zj_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
  • KVO监听

这部分主要是通过 重写setter方法,在中间类的setter方法中,通过 block 方式传递给外部进行响应

  • 移除观察者
1
- (void)zj_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

注册观察者

在注册观察者步骤中,主要有以下几步操作:

  • 1、验证是否存在 setter 方法,目的是 不让实例进来
  • 2、动态生成子类,将需要重写的 class 方法添加到中间类中
  • 3、isa 指向原有类,改为 指向中间类
  • 4、保存信息:这里用的是数组,也可以使用map,需要创建信息的 model模型类

【注意】:

关于 objc_msgSend 的检查关闭:target -> Build Setting -> Enable Strict Checking of objc_msgsend Calls 设置为 NO

KVO响应

  1. 主要是给 子类 动态添加 setter 方法,其目的是为了在 setter方法 中向父类发送消息,告知其属性值的变化

  2. 通过系统的 objc_msgSendSuper 强制类型转换自定义的消息发送 zj_msgSendSuper

  3. 告知vc去响应:获取信息,通过block传递

移除观察者

为了避免在外界不断的调用 removeObserver 方法,在自定义 KVO 中实现 自动移除观察者

  • 1、实现 zj_removeObserver:forKeyPath: 方法,主要是清空数组,以及 isa 指向更改

  • 2、在子类中重写 dealloc 方法,当子类销毁时,会自动调用dealloc方法(在动态生成子类的方法中添加)

其主要原理是:ZJPerson 发送消息释放即 dealloc 了,就会自动走到重写的 zj_dealloc 方法中(原因是person对象的 isa 指向变了),指向中间类,但是实例对象的地址是不变的,所以子类的释放,相当于释放了外界的person,而重写的 zj_dealloc 相当于是 重写了ZJPersondealloc 方法,所以会走到 zj_dealloc 方法中,达到自动移除观察者的目的。

完整的代码

  • 【NSObject+ZJKVO.h】
1
2
3
4
5
6
7
typedef void(^ZJKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);

@interface NSObject (LGKVO)
- (void)zj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LGKVOBlock)block;
- (void)zj_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end
  • 【NSObject+ZJKVO.m】
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#import "NSObject+ZJKVO.h"
#import <objc/message.h>

static NSString *const kZJKVOPrefix = @"ZJKVONotifying_";
static NSString *const kZJKVOAssiociateKey = @"kZJKVO_AssiociateKey";

#*****信息model*****
@interface ZJInfo : NSObject
@property (nonatomic,weak)NSObject * observer;
@property (nonatomic,copy)NSString * keyPath;
@property (nonatomic,copy)ZJKVOBlock handleBlock;
@end
@implementation ZJInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(ZJKVOBlock)block{
if (self=[super init]) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}
@end

@implementation NSObject (ZJKVO)
- (void)zj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(ZJKVOBlock)block{
// 1.验证是否存在setter方法:不让实例进来
[self judgeSetterMethodFromKeyPath:keyPath];
// 2.动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
// 3.isa的指向 : ZJKVONotifying_ZJPerson
object_setClass(self, newClass);
// 4.保存信息
ZJInfo * info = [[ZJInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
NSMutableArray * mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kZJKVOAssiociateKey));
if (!mArray) {
mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kZJKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
}

#pragma mark -移除观察者
- (void)zj_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kZJKVOAssiociateKey));
if (observerArr.count<=0) {
return;
}
for (ZJInfo *info in observerArr) {
if ([info.keyPath isEqualToString:keyPath]) {
[observerArr removeObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kZJKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
break;
}
}
if (observerArr.count<=0) {
// 指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}
}

#pragma mark - 验证是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
Class superClass = object_getClass(self); // 获取类Person
SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
if (!setterMethod) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"老铁没有当前%@的setter",keyPath] userInfo:nil];
}
}

#pragma mark -动态生成子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
// 获取原本的类名(Person)
NSString * oldClassName = NSStringFromClass([self class]);
// 拼接子类类名(ZJKVONotifying_Person)
NSString * newClassName = [NSString stringWithFormat:@"%@%@",kZJKVOPrefix,oldClassName];
// 获取子类
Class newClass = NSClassFromString(newClassName);
// 防止重复创建生成新类
if (newClass) return newClass;
/**
* 如果内存不存在,创建生成
* 参数一: 父类
* 参数二: 新类的名字
* 参数三: 新类的开辟的额外空间
*/
// 2.1 : 申请类(ZJKVONotifying_Person)
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);

// 2.2 : 注册类
objc_registerClassPair(newClass);

// 2.3.1 : 添加class : class的指向是ZJPerson
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)zj_class, classTypes);

// 2.3.2 : 添加setter
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath)); // 获取sel
Method setterMethod = class_getInstanceMethod([self class], setterSEL); // 获取setter实例方法
const char *setterTypes = method_getTypeEncoding(setterMethod); // 方法签名
class_addMethod(newClass, setterSEL, (IMP)zj_setter, setterTypes); // 添加一个setter方法

// 2.3.3 : 添加dealloc
SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
const char *deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSEL, (IMP)zj_dealloc, deallocTypes);

return newClass;
}

#pragma mark -重写class方法,为了与系统类对外保持一致
Class zj_class(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}

#pragma mark -添加setter方法
static void zj_setter(id self,SEL _cmd,id newValue){
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
// 4: 消息转发 : 转发给父类
// 通过系统强制类型转换自定义objc_msgSendSuper
void (*zj_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
// 定义一个结构体
struct objc_super superStruct = {
.receiver = self, // 消息接收者为当前的self
.super_class = class_getSuperclass(object_getClass(self)), // 当第一次快捷查找的类为父类
};
// 调用自定义的放消息函数
zj_msgSendSuper(&superStruct,_cmd,newValue);

// 5: 信息数据回调
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kZJKVOAssiociateKey));
for (ZJInfo *info in mArray) {
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
info.handleBlock(info.observer, keyPath, oldValue, newValue);
}
}
}

#pragma mark - 从get方法获取set方法的名称 key -> setKey:
static NSString *setterForGetter(NSString *getter){
if (getter.length <= 0) { return nil;}
NSString *firstString = [[getter substringToIndex:1] uppercaseString];
NSString *leaveString = [getter substringFromIndex:1];

return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}

#pragma mark - 从set方法获取getter方法的名称 set<Key>:===> key
static NSString *getterForSetter(NSString *setter){
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
NSRange range = NSMakeRange(3, setter.length-4);
NSString *getter = [setter substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];

return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}

#pragma mark -重写dealloc
static void zj_dealloc(id self,SEL _cmd){
NSLog(@"[%@ dealloc]",self);
Class superClass = [self class];
object_setClass(self, superClass);
}

@end

总结

综上所述,自定义KVO大致分为一下几步:

  • 注册观察者 & 响应

    • 验证是否存在 setter 方法
    • 动态生成子类,需要重写 class、setter 方法
    • 在子类的setter方法中向父类发消息,即 自定义消息发送
    • isa原有类指向中间类
    • 保存信息
    • 让观察者 响应
  • 移除观察者

    • 更改 isa指向 为原有类
    • 重写子类的 dealloc 方法
  • Post title:OC底层原理23:KVO底层原理分析
  • Post author:张建
  • Create time:2021-01-25 16:34:11
  • Post link:https://redefine.ohevan.com/2021/01/25/OC底层原理/OC底层原理23:KVO底层原理分析/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.