什么是KVO?
KVO是Key-Value Observing
的简写,称为键值观察
,用来监听对象属性的变化
。
KVO使用探究
我们以IFPerson
类为例研究KVO
的使用。
自动触发KVO
监听
- 主要代码
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person.name = @"kvo";
NSLog(@"class = %@",object_getClass(self.person));
//对person的name添加kvo监听
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
NSLog(@"class = %@",object_getClass(self.person));
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = [NSString stringWithFormat:@"%@ +",self.person.name];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@" === %@",change);
}
}
-(void)dealloc {
//移除监听
[self.person removeObserver:self forKeyPath:@"name"];
}
运行代码点击屏幕效果如下
手动触发KVO
监听
如果想要手动触发KVO
监听,需要重写+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
函数,该函数默认返回YES,表示自动触发,返回NO,则表示手动触发
。对于本文,我们判断下key == name
则手动触发,否则自动触发。修改代码如下
//在`IFPerson.m`中
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
-(void)setName:(NSString *)name {
[self willChangeValueForKey:name];
_name = name;
[self didChangeValueForKey:name];
}
从上述代码中,我们可以看到,要实现手动触发KVO
,有两个步骤需要实现
步骤一
:重写+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
函数,根据需要返回NO步骤二
:对于监听的对象属性发生改变时,需要在改变前后调用willChangeValueForKey:
和didChangeValueForKey:
KVO
底层原理探究
从上述中我们可以看到我们可以根据KVO
监听属性的变化,那么在底层是怎么实现的呢?
KVO
监听前后对象的变化
我们在监听name
前后获取person
的class以及其指针地址
看下是否有变化,代码如下
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person.name = @"kvo";
NSLog(@"class = %@ address = %p",object_getClass(self.person),self.person);
//对person的name添加kvo监听
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
NSLog(@"class = %@ address = %p",object_getClass(self.person),self.person);
}
打印结果如下从上图中我们看到,
self.person
的class
前后发生了改变,但是对象地址并没有改变。我们再通过获取isa
来验证下。
- 监听之前
- 监听之后
- 监听前后总结:从上面分析可知,在进行
KVO
监听后,对象的class即isa指向发生了改变
,由IFPerson
变成了NSKVONotifying_IFPerson
,但是对象的地址没有改变
。
NSKVONotifying_IFPerson
是什么?
我们知道了在进行KVO
监听后class
变成了NSKVONotifying_IFPerson
,那么NSKVONotifying_IFPerson
从何而来?下面我们看下NSKVONotifying_IFPerson
的父类是谁。
准备工作
由于superClass
是隐藏属性,在外部无法访问,因为我们定义一个结构体,并强转下类型来获得superClass
,代码如下
struct cw_objc_class {
Class _Nonnull isa;
Class _Nullable super_class;
};
// 强转一下类型
struct cw_objc_class *newClass = (__bridge struct cw_objc_class *)object_getClass(self.person);
NSLog(@"superClass = %@",newClass->super_class);
从打印结果来看
NSKVONotifying_IFPerson
的父类竟然是IFPerson
。
NSKVONotifying_IFPerson
类实现了哪些方法?
既然runtime
在进行KVO
监听的时候派生了NSKVONotifying_IFPerson
类,那么NSKVONotifying_IFPerson
类跟IFPerson
类实现方法有什么区别呢?我们打印下监听前后两个类的实现方法区别
#pragma mark - 遍历方法-ivar-property
- (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);
}
NSLog(@"监听之前方法:");
[self printClassAllMethod:object_getClass(self.person)];
//对person的name添加kvo监听
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
NSLog(@"监听之前方法:");
[self printClassAllMethod:object_getClass(self.person)];
打印结果如下从打印监听前后方法列表我们可以看到
NSKVONotifying_IFPerson
类有如下变化
- 重写了被监听属性的
setter
方法,如setName: - 修改了
isa
指针指向的地址,由原来的IFPerson
指向了NSKVONotifying_IFPerson
- 实现了
dealloc
方法 - 增加了一个
_isKOA
标识符
isa
指针何时重新指向IFPerson
?
既然isa
指针在被监听后指向了NSKVONotifying_IFPerson
类,那么何时重新指向IFPerson
呢?答案是在移除监听的时候
。为了验证,我们在dealloc
中打印下监听前后isa
指针的变化。
KVO
总结
KVO
在监听属性后,runtime会根据对象的类型生成一个NSKVONotifying_XXX(XXX是原来的类)
的派生类
,该派生类NSKVONotifying_XXX
继承于原来的类
NSKVONotifying_XXX
重写被监听属性的setter
方法,但是赋值
的时候通过重写的setter
内部消息转发还是通过原来类的setter
方法来实现KVO
在移除监听时将isa
指针指向地址由NSKVONotifying_XXX
派生类重新指向原来类