本文为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"];
}
结果 :
结论 :
KVO
观察的是拥有setter
方法的变量,可以是成员变量,但是必须使用KVC
进行赋值,属性则是可以直接赋值进行观察的。
二、isa-swizzling
还是上面的代码,在ViewController
中的添加属性的观察者
的代码行加上断点。
利用lldb
,po
看一下self.person
的类是否有改变。
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.pngNSKVONotifying_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
是否一直指向中间类。
结果 :
很明显,当观察者被移除之后,被观察的对象的isa
重新指回了原来的类。
问题 :
中间类会被直接销毁吗?
操作 :
在移除观察者的代码下面,调用printClassSubClassList
,打印查看JDPerson
类的所有子类。
结果 :
所以是没有销毁的,这种缓存的做法也减少了下次再添加观察者的时候的开支。
结论 :
NSKVONotifying_xxx
这个中间子类会重写父类的setter
、dealloc
、class
方法。并且当观察者移除之后,中间类并不会被销毁,而是缓存起来,有需要的时候直接调用。
四、总结
KVO
观察对象的特定属性发生变化的核心思想是利用isa-swizzling
.- 被观察的特定属性所属的对象的
isa
会指向一个动态生成的中间类,并且中间类拥有一个统一的前缀NSKVONotifying_
,中间类继承于对象的父类
。- 中间子类会重写3个方法 :
setter
、class
、dealloc
,另外会自带一个判断是否是KVO
生成的子类的方法 :_isKVOA
。- 当移除观察的时候,被观察的属性所属的对象的
isa
会重新指会原本的类。- 生成的中间子类不会被销毁,依然存在于原来类的缓存之中。
网友评论