本文为L_Ares个人写作,以任何形式转载请表明原文出处。
准备 :
KVO官方文档——KVO实现细节。
看了一下官方文档关于KVO实现细节的描述,内容很少,但是也阐明了其实现的核心思想——isa-swizzling。
这里先翻译一下 :
KVO是通过isa-swizzling思想实现的。isa指向对象的类(不明白的看这里),这个类拥有着dispatch_table,dispatch_table存储指针,这个指针指着类中的方法的实现(imp),还指着其他的一些数据。- 当一个
对象的属性被添加了观察者之后,对象的isa指针被修改,指向一个中间类,而不是指向真正的类。因此,isa指针的值不一定反映的就是实例对象的实际类。(在KVO这里的一个小彩蛋,和之前说isa指向的时候有一点点小不一样)。- 官方建议 : 永远不要依据
isa指针来决定类的成员关系。应该依据class方法来确定实例对象的类。
OK,那么这里其实已经说出了KVO的核心实现思想——isa交换实现。
一、明确 : KVO只观察实现setter的变量
- 随意创建一个
Project,创建一个类,类拥有一个属性,一个成员变量。 - 实例化一个类的对象,并添加属性和成员变量的观察者都为
ViewController。 - 添加
touchBegin方法,做到点击屏幕,就让属性和实例变量都发生变化。
JDPerson
/************************************JDPerson.h************************************/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface JDPerson : NSObject
{
@public
NSString *jd_name;
}
@property (nonatomic, copy) NSString *jd_nickName;
@end
NS_ASSUME_NONNULL_END
/************************************JDPerson.m************************************/
#import "JDPerson.h"
@implementation JDPerson
- (void)setJd_nickName:(NSString *)jd_nickName
{
_jd_nickName = jd_nickName;
}
@end
ViewController.m
#import "ViewController.h"
#import "JDPerson.h"
@interface ViewController ()
@property (nonatomic, strong) JDPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self jd_kvo_init_class];
[self jd_kvo_add_observer];
}
#pragma mark - 初始化
- (void)jd_kvo_init_class
{
self.person = [[JDPerson alloc] init];
self.person->jd_name = @"";
self.person.jd_nickName = @"";
}
#pragma mark - 添加观察者
- (void)jd_kvo_add_observer
{
//添加属性的观察者
[self.person addObserver:self forKeyPath:@"jd_nickName" options:NSKeyValueObservingOptionNew context:NULL];
//添加成员变量的观察者
[self.person addObserver:self forKeyPath:@"jd_name" options:NSKeyValueObservingOptionNew context:NULL];
}
#pragma mark - 让被观察的属性发生变化
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person->jd_name = @"changed_name_ljd";
self.person.jd_nickName = @"changed_nick_name_LJD";
}
#pragma mark - 观察回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"观察回调 : %@",change);
}
#pragma mark - dealloc
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"jd_name"];
[self.person removeObserver:self forKeyPath:@"jd_nickName"];
}
@end
结果 :
图1.0.0.png
再操作 :
利用KVC给成员变量jd_name赋值 :
#pragma mark - 让被观察的属性发生变化
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person->jd_name = @"changed_name_ljd";
self.person.jd_nickName = @"changed_nick_name_LJD";
[self.person setValue:@"kvc_changed_name_ljd" forKey:@"jd_name"];
}
结果 :
图1.0.1.png
结论 :
KVO观察的是拥有setter方法的变量,可以是成员变量,但是必须使用KVC进行赋值,属性则是可以直接赋值进行观察的。
二、isa-swizzling
还是上面的代码,在ViewController中的添加属性的观察者的代码行加上断点。
利用lldb,po看一下self.person的类是否有改变。
图2.0.0.png
self.person的类发生了变化,isa从指向着JDPerson,变为了指向NSKVONotifying_JDPerson。
问题 :
那么NSKVONotifying_JDPerson这个类是一个什么性质的类?
思路 :
正常的来推测,它应该属于JDPerson的一个子类,为了验证,可以来获取JDPerson的子类列表,查看添加观察者前和添加观察者后,JDPerson的子类列表是否出现差异,是否增加了一个NSKVONotifying_JDPerson类 。
操作 :
在ViewController中添加如下代码 :
#pragma mark - 获取类的子类列表
- (void)printClassSubClassList:(Class)cls
{
//获取注册类的总数量
int count = objc_getClassList(NULL, 0);
//创建一个可变数组,包含着给定的类
NSMutableArray *mutArr = [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])) {
[mutArr addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@",mutArr);
}
并在添加观察者之前和之后分别调用,传入参数[JDPerson class],查看JDPerson的子类。
结果 :
图2.0.1.png
结论 :
KVO给对象的特定属性添加观察者之后,对象的isa指向了一个中间类,并且这个中间类是对象所属类的子类。
三、KVO的中间子类
上面也看到了,当给一个对象的属性添加了观察之后,会发现该对象的isa指向发生了改变,指向了一个继承于该对象父类的子类,并且以NSKVONotifying_作为前缀。
那么这里就要看一下这个NSKVONotifying_作为前缀的类都做了什么,才实现了KVO的观察能力。既然NSKVONotifying_xxx是一个类,它必然拥有着isa、superClass、cache_t、bits,这4个基本的要素。
superClass都在上面说过了。
isa,既然是类的isa那么必然指向的是元类,元类再指向根元类。
cache_t则不知道在KVO里面这个中间类要怎么用。
bits,因为KVO主要做的事情就是观察变化,观察setter,所以终点一定在bits这里,只有它拥有着属性、方法的钥匙。
先看一下中间类的方法是否对比原来的类的方法有发生一些变化。
3.1 中间子类的方法
操作 :
添加如下代码获取类的方法列表 :
#pragma mark - 获取类的方法列表
- (void)printClassMethodsList:(Class)cls
{
NSLog(@"***************分割线上****************");
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);
NSLog(@"***************分割线下****************");
}
该方法放在注册观察者前调用一次,在注册观察者后再调用一次。
结果如下 :
图3.1.0.png
NSKVONotifying_xxx这个中间类拥有着4个方法 :
-
setXxx: 重写被观察的属性的set方法。 -
class: 重写自己的class方法。 -
dealloc: 重写自己的dealloc方法。 -
_isKVOA: 判断是不是KVO生成的中间类。
结论 :
也就是说,在进行了
KVO观察之后的对象,它的isa再指向的就是NSKVONotifying_xxx这个类,做的改变都会找到NSKVONotifying_xxx的set方法来对属性进行更改。
3.2 中间子类的存在与销毁
现在只观察jd_nickName这个属性,不再观察jd_name这个成员变量了。
操作 :
给jd_nickName发送通知之后,直接就移除观察者,来po一下self.person的类,看看添加过观察者的对象的isa是否一直指向中间类。
图3.2.0.png
结果 :
很明显,当观察者被移除之后,被观察的对象的isa重新指回了原来的类。
问题 :
中间类会被直接销毁吗?
操作 :
在移除观察者的代码下面,调用printClassSubClassList,打印查看JDPerson类的所有子类。
结果 :
图3.2.1.png
所以是没有销毁的,这种缓存的做法也减少了下次再添加观察者的时候的开支。
结论 :
NSKVONotifying_xxx这个中间子类会重写父类的setter、dealloc、class方法。并且当观察者移除之后,中间类并不会被销毁,而是缓存起来,有需要的时候直接调用。
四、总结
KVO观察对象的特定属性发生变化的核心思想是利用isa-swizzling.- 被观察的特定属性所属的对象的
isa会指向一个动态生成的中间类,并且中间类拥有一个统一的前缀NSKVONotifying_,中间类继承于对象的父类。- 中间子类会重写3个方法 :
setter、class、dealloc,另外会自带一个判断是否是KVO生成的子类的方法 :_isKVOA。- 当移除观察的时候,被观察的属性所属的对象的
isa会重新指会原本的类。- 生成的中间子类不会被销毁,依然存在于原来类的缓存之中。











网友评论