KVO的全称是Key-Value Observing
,俗称"键值监听",可以用于监听摸个对象属性值得改变。
KVO一般通过以下三个步骤使用:
// 1. 添加监听
[self.student1 addObserver:self forKeyPath:@"age" options:options context:nil];
// 2. 重写- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@的%@被改变:%@", object, keyPath, change);
}
// 3. 适当时机移除监听
[self.student1 removeObserver:self forKeyPath:@"age"];
SXStudent
类和SXTeacher
类//SXStudent.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SXStudent : NSObject
@property (nonatomic, assign) NSInteger age;
@end
NS_ASSUME_NONNULL_END
// SXTeacher.h
#import <Foundation/Foundation.h>
#import "SXStudent.h"
NS_ASSUME_NONNULL_BEGIN
@interface SXTeacher : NSObject
@property (nonatomic, strong) SXStudent *student1;
@property (nonatomic, strong) SXStudent *student2;
- (void)demo;
@end
NS_ASSUME_NONNULL_END
SXStudent
类。// SXStudent.m
#import "SXStudent.h"
@implementation SXStudent
@end
SXTeacher
类,重写init
方法,为SXTeacher
的student1
属性添加监听。实现demo
方法,分别更改student1
和student2
的age
值。// SXTeacher.m
#import "SXTeacher.h"
#import <objc/runtime.h>
@implementation SXTeacher
- (id)init {
if (self = [super init]) {
self.student1 = [[SXStudent alloc] init];
self.student2 = [[SXStudent alloc] init];
self.student1.age = 1;
self.student2.age = 2;
// 添加监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.student1 addObserver:self forKeyPath:@"age" options:options context:nil];
}
return self;
}
- (void)demo {
self.student1.age = 20;
self.student2.age = 30;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@的%@被改变:%@", object, keyPath, change);
}
- (void)dealloc {
// 移除监听
[self.student1 removeObserver:self forKeyPath:@"age"];
}
@end
mian
函数内创建SXTeacher
的实例对象并调用demo
方法测试。#import <Foundation/Foundation.h>
#import "SXTeacher.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
SXTeacher *teacher = [[SXTeacher alloc] init];
[teacher demo];
}
return 0;
}
为什么student1
的setter
方法可以触发监听,添加监听的方法到底对student1
做了什么?
我们发现student1
的isa
指针的指向被更改成了NSKVONotifying_SXStudent
(NSKVONotifying_为前缀,原类名为后缀)类。
NSKVONotifying_XXX
类NSKVONotifying_XXX
类是 Runtime
动态创建的一个类,在程序运行的过程中产生的一个新的类。NSKVONotifying_XXX
类是原类的一个子类。NSKVONotifying_XXX
类存在自己的 setAge:
、class
、dealloc
、isKVOA
…方法。试着验证NSKVONotifying_XXX
类的方法和父类,我们可以使用如下代码打印NSKVONotifying_SXStudent
类和SXStudent
类的方法列表和父类类型。
- (void)demo2 {
[self printMethods:object_getClass(self.student1)];
[self printMethods:object_getClass(self.student2)];
}
- (void) printMethods:(Class)cls {
unsigned int count;
Method *methods = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [NSMutableString string];
[methodNames appendFormat:@"%@ - ", cls];
NSLog(@"%@ superClass ----> %@", NSStringFromClass(cls), NSStringFromClass(class_getSuperclass(cls)));
for (int i = 0; i < count; i++) {
Method method = methods[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methodNames appendFormat:@"%@ ", methodName];
}
NSLog(@"%@", methodNames);
free(methods);
}
可以看到NSKVONotifying_SXStudent
类有自己的setAge:
、class
、dealloc
、 _isKVOA
方法。
- 重写class方法是为了隐藏NSKVONotifying_XXX类的存在。重写后的class方法返回其父类(原来的类)类型,使用户以为类没有变化。
- _isKVOA则是用来标识当前类是否是通过runtime动态生成的类对象,如果是,就返回YES,不是,则返回NO。
- 当对像被销毁后,dealloc做一些收尾工作。
由上面可分析出我们的student1
的isa
指针指向的类对象是NSKVONotifying_SXStudent
,并且NSKVONotifying_SXStudent
中还带有setAge:
方法,所以student1
的setAge:
方法走的应该是NSKVONotifying_SXStudent
类中的setAge:
方法。
我们试着使用下面的代码打印student1
被监听前后的setAge:
方法的地址,并使用lldb调试一探究竟。
- (id)init {
if (self = [super init]) {
self.student1 = [[SXStudent alloc] init];
self.student2 = [[SXStudent alloc] init];
self.student1.age = 1;
self.student2.age = 2;
NSLog(@"添加监听之前 - p1 = %p, p2 = %p", [self.student1 methodForSelector:@selector(setAge:)], [self.student2 methodForSelector:@selector(setAge:)]);
// 添加监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.student1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"添加监听之后 - p1 = %p, p2 = %p", [self.student1 methodForSelector:@selector(setAge:)], [self.student2 methodForSelector:@selector(setAge:)]);
}
return self;
}
我们发现student1
的setAge:
方法实际上是调用了Foundation
框架的_NSSetLongLongValueAndNotify
函数。这又是怎么回事,我们先来了解一下这个函数。
经过查阅资料我们可以了解到。
NSKVONotifyin_XXX
中的setage:
方法中其实调用了Fundation
框架中C语言函数_NSsetXXXValueAndNotify
,_NSsetXXXValueAndNotify
内部做的操作相当于,首先调用willChangeValueForKey
将要改变方法,之后调用原来的setage
方法对成员变量赋值,最后调用didChangeValueForKey
已经改变方法。didChangeValueForKey
中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath
方法中。
Foundation
框架中会根据属性的类型,调用不同的方法。例如我们之前定义的NSInteger
类型的age
属性,那么我们看到Foundation
框架中调用的_NSsetLongLongValueAndNotify
函数。那么我们把age
的属性类型变为double
重新打印一遍。
我们发现调用的函数变为了_NSSetDoubleValueAndNotify
,那么这说明Foundation
框架中有许多此类型的函数,通过属性的不同类型调用不同的函数。
我们可以重写 SXStudent
类的willChangeValueForKey
方法和didChangeValueForKey
方法来验证上述说法。
#import "SXStudent.h"
@implementation SXStudent
- (void)setAge:(NSInteger)age {
NSLog(@"setAge");
_age = age;
}
- (void)willChangeValueForKey:(NSString *)key {
NSLog(@"willChangeValueForKey begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey end");
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValueForKey begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey end");
}
@end
可知:
_NSSetXXXValueAndNotify
调用willChangeValueForKey:
;_NSSetXXXValueAndNotify
调用setter
实现;_NSSetXXXValueAndNotify
调用didChangeValueForKey:
;didChangeValueForKey
内部会调用observe
r的observeValueForKeyPath:ofObject:change:context:
方法。据上所述,可以写出NSKVONotifying_SXStudent类的伪代码:
///> NSKVONotifying_SXStudent.m 文件
#import "NSKVONotifying_SXStudent.h"
@implementation NSKVONotifying_SXStudent
- (void)setAge:(int)age{
_NSSetLongLongValueAndNotify(); ///> 文章末尾 知识点补充小结有此方法来源
}
void _NSSetLongLongValueAndNotify(){
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key{
///> 通知监听器 key发生了改变
[observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end
KVC的全称key - value - coding,俗称"键值编码",可以通过key来访问某个属性。
常见的API有:
- (void)setValue:(id)value forKey:(NSString *)key;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key
- (id)valueForKeyPath:(NSString *)keyPath;
// SXDog.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SXDog : NSObject
@property (nonatomic, assign) CGFloat weight;
@end
NS_ASSUME_NONNULL_END
// SXStudent.h
#import <Foundation/Foundation.h>
#import "SXDog.h"
NS_ASSUME_NONNULL_BEGIN
@interface SXStudent : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) SXDog *dog;
@end
NS_ASSUME_NONNULL_END
// SXTeacher.h
#import <Foundation/Foundation.h>
#import "SXStudent.h"
NS_ASSUME_NONNULL_BEGIN
@interface SXTeacher : NSObject
@property (nonatomic, strong) SXStudent *student1;
@end
NS_ASSUME_NONNULL_END
SXDog.m
#import "SXDog.h"
@implementation SXDog
@end
SXStudent.m
#import "SXStudent.h"
#import <objc/runtime.h>
@implementation SXStudent
- (id)init {
if (self = [super init]) {
self.dog = [[SXDog alloc] init];
}
return self;
}
@end
SXTeacher.m
#import "SXTeacher.h"
#import <objc/runtime.h>
@implementation SXTeacher
- (id)init {
if (self = [super init]) {
self.student1 = [[SXStudent alloc] init];
}
return self;
}
@end
SetValue:ForKey:
与ValueForKey:
方法- (void)demoSetValueForKeyAndValueForKey {
[self.student1 setValue:@20 forKey:@"age"];
NSLog(@"点语法:%ld", self.student1.age);
NSNumber *value = [self.student1 valueForKey:@"age"];
NSLog(@"KVC:%@", value);
}
SetValue:ForKeyPath:
与ValueForKeyPath:
- (void)demoSetValueForKeyPathAndValueForKeyPath {
[self.student1 setValue:@16 forKeyPath:@"dog.weight"];
NSLog(@"点语法:%lf", self.student1.dog.weight);
NSNumber *value = [self.student1 valueForKeyPath:@"dog.weight"];
NSLog(@"KVC:%@", value);
}
keyPath
相当于根据路径去寻找属性,一层一层往下找。key
是直接访问属性的名字,如果按路径找会报错。accessInstanceVariablesDirectly函数
+ (BOOL)accessInstanceVariablesDirectly{
return YES; ///> 可以直接访问成员变量
// return NO; ///> 不可以直接访问成员变量,
///> 直接访问会报NSUnkonwKeyException错误
}
对上述例子进行小修改:
SXStudent.h
@interface SXStudent : NSObject {
@public
int _age;
int _isAge;
int age;
int isAge;
}
@end
SXTeacher.m
- (id)init {
if (self = [super init]) {
self.student1 = [[SXStudent alloc] init];
NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.student1 addObserver:self forKeyPath:@"age" options:option context:nil];
[self.student1 setValue:@20 forKey:@"age"];
NSLog(@"-----");
}
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@的%@被改变:%@", object, keyPath, change);
}
在NSLog(@"-----");
处打下断点,运行,查看student1
中的成员变量。看看谁被赋值了。
可以看到_age
首先被赋值,我们注释掉SXStudent
中的_age
成员变量,看看下一个是谁被赋值。如此反复,就可以得到setValue:forkey:
赋值流程。结果与上述无误,我就不继续了。
通过本例,我们还可以知道KVC也可以触发KVO监听。
给一个实例对象添加KVO,系统内部是利用Runtime
动态的生成一个此实例对象的类对象的子类,具体的格式为_NSKVONotifying_XXX
,并且让实例对象的isa
指针指向这个新生成的类。
重写属性的set
方法,当调用set
方法时,会调用Foundation
框架的NSSetXXXValueAndNotify
函数
在_NSSetXXXValueAndNotify
中会执行以下步骤:
willChangeValueForKey:
方法;set
方法,重新赋值;didChangeValueForKey:
方法;didChangeValueForKey:
内部会触发监听器的observeValueForKeyPath:ofObject:change:context:
方法。手动调用willChangeValueForKey:
和didChangeValueForKey:
。
例:
- (id)init {
if (self = [super init]) {
self.student1 = [[SXStudent alloc] init];
NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.student1 addObserver:self forKeyPath:@"age" options:option context:nil];
[self.student1 willChangeValueForKey:@"age"];
[self.student1 didChangeValueForKey:@"age"];
}
return self;
}
运行结果:
虽然是在didChangeValueForKey:
内部会触发监听器的observeValueForKeyPath:ofObject:change:context:
方法,但是如果不调用willChangeValueForKey:
无法就无法触发监听器,这两个必须一起使用。
直接修改成员变量的值不会触发KVO,因为没有触发setter方法。