前言
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
会自动通知响应的管擦着,那么 KVO
与 NSNotificationCenter
有什么区别呢?
相同点
- 两者的实现原理
都是观察者模式
,都是用于 监听
- 都能
实现一对多
的操作
不同点
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];
|
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
通知回调的,如下代码是不会通知回调的:
1 2 3
| @interface Person : NSObject @property (nonatomic,strong)NSMutableArray * mArr; // 可变数组 @end
|
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
和 可变数组
为例
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]; }
|
1 2 3 4
| - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ self.person->name = @"ZJ"; self.person.nick = @"XJ"; }
|
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指针指向会发生改变
- 注册观察者之前:
实例对象person
的 isa
指针指向 ZJPerson
- 注册观察者之后:
实例对象person
的 isa
指针指向 NSKVONotifing_ZJPerson
综上所述,在注册观察者后,实例对象
的 isa
指针 指向
由 ZJPerson类
变为了 NSKVONotifying_ZJPerson中间类
,即实例对象的 isa
指针指向发生了变化
判断中间类是否是 派生类
即 子类?
那么这个动态生成的中间类 NSKVONotifying_ZJPerson
和 ZJPerson
有什么关系?下面通过代码来验证
可以通过下面封装的方法,获取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_ZJPerson
是 ZJPerson
的子类
中间类中有什么?
可以通过下面的方法获取 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
打印的结果一致
- 获取
ZJPerson
和 NSKVONotifying_ZJPerson
的方法列表进行对比
【结论】 综上所述,有如下结论:面试重点
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
|
这部分主要是通过 重写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响应
主要是给 子类
动态添加 setter
方法,其目的是为了在 setter方法
中向父类发送消息,告知其属性值的变化
通过系统的 objc_msgSendSuper
强制类型转换自定义的消息发送 zj_msgSendSuper
告知vc去响应:获取信息,通过block传递
移除观察者
为了避免在外界不断的调用 removeObserver
方法,在自定义 KVO
中实现 自动移除观察者
其主要原理是:ZJPerson
发送消息释放即 dealloc
了,就会自动走到重写的 zj_dealloc
方法中(原因是person对象的 isa
指向变了),指向中间类,但是实例对象的地址是不变的,所以子类的释放,相当于释放了外界的person,而重写的 zj_dealloc
相当于是 重写了ZJPerson
的 dealloc
方法,所以会走到 zj_dealloc
方法中,达到自动移除观察者的目的。
完整的代码
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
|
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
方法