一、KVO
KVO 的全称是 Key-Value Observing,俗称 “键值监听”,可以用于监听某个对象属性值的改变。
下面用一个代码例子来总结原理,首先有一个 MyPerson 类,里面只有一个 age 年龄属性:
@interface MyPerson : NSObject
@property (assign, nonatomic) int age;
@end
然后在外部控制器中我们使用 MyPerson 声明两个示例对象 person1 和 person2。只对 person1 添加 KOV 监听,然后通过打印监听前和监听后的内存地址,来看他们的 isa
指针指向是否有变化。并且打印 person1 和 person2 的类对象和元类对象进行对比。
@interface ViewController ()
@property (strong, nonatomic) MyPerson *person1;
@property (strong, nonatomic) MyPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MyPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MyPerson alloc] init];
self.person2.age = 2;
NSLog(@"person1添加KVO监听之前 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2));
NSLog(@"person1添加KVO监听之前 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
NSLog(@"person1添加KVO监听之后 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2));
NSLog(@"person1添加KVO监听之后 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
NSLog(@"类对象 - %@ %@",
object_getClass(self.person1), // self.person1.isa
object_getClass(self.person2)); // self.person2.isa
NSLog(@"元类对象 - %@ %@",
object_getClass(object_getClass(self.person1)), // self.person1.isa.isa
object_getClass(object_getClass(self.person2))); // self.person2.isa.isa
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// NSKVONotifying_MyPerson是使用Runtime动态创建的一个类,是MyPerson的子类
// self.person1.isa == NSKVONotifying_MyPerson
//[self.person1 setAge:21];
self.person1.age = 20;
self.person2.age = 20;
}
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
打印结果:
person1添加KVO监听之前 - MyPerson MyPerson
person1添加KVO监听之前 - 0x1078b6440 0x1078b6440
person1添加KVO监听之后 - NSKVONotifying_MyPerson MyPerson
person1添加KVO监听之后 - 0x7fff207bf79f 0x1078b6440
类对象 - NSKVONotifying_MyPerson MyPerson
元类对象 - NSKVONotifying_MyPerson MyPerson
我们发现添加KVO监听之前,person1 和 person2 的类对象是一样的都是 MyPerson
,但是添加 KVO 监听之后,person1 的类对象变为了NSKVONotifying_MyPerson
。
未使用KVO监听的对象:
使用了 KVO 监听的对象:
_setAge
其实是调用了_NSSetIntValueAndNotify
方法,
_NSSetIntValueAndNotify
方法内部其实是如下伪代码:
// 伪代码
void _NSSetIntValueAndNotify() {
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key {
// 通知监听器,某某属性值发生了改变
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nol];
}
Foundation
框架内除了有 _NSSetIntValueAndNotify
,还有_NSSetBoolValueAndNotify
、_NSSetCharValueAndNotify
、_NSSetDoubleValueAndNotify
等等类型。
_NSSetxxxValueAndNotify 的内部实现
调用
willChangeValueForKey:
调用原来的
setter
实现调用
didChangeValueForKey: didChangeValueForKey:
内部会调用observer
的observeValueForKeyPath:ofObject:change:context:
方法
NSKVONotifying_MyPerson
除了重写了 setAge:
方法,还重写了 class、dealloc、_isKVOA
方法。如果调用下面的代码:
NSLog(@"%@ %@", object_getClass(self.person1), object_getClass(self.person2));
NSLog(@"%@ %@", [self.person1 class]), [self.person2 class]);
会发现打印为:
NSKVONotifying_MyPerson MyPerson
MyPerson MyPerson
所以说明如果是通过 rumtime
的函数 objc_getClass
拿出来的是它真正的 isa
指向,而 class
方法则不是,因为它重写了class
方法,屏蔽了内部实现,隐藏了 NSKVONotifying_MyPerson
类的存在。
我们可以通过下面的方式证明一下 NSKVONotifying_MyPerson
是否重写了这几个方法,我们可以打印一下这个类的所有方法名:
- (void)printMethodNamesOfClass:(Class)cls {
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[I];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 释放
free(methodList);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
打印结果为:
NSKVONotifying_MyPerson setAge:, class, dealloc, _isKVOA,
MyPerson setAge:, age,
KVO 相关面试题:
1、iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
答: 利用 RuntimeAPI 动态生成一个子类,并且让 instance 对象的 isa 指向这个全新的子类
当修改 instance 对象的属性时,会调用 Foundation 的_NSSetXXXValueAndNotify
函数
{
willChangeValueForKey:
父类原来的 setter
didChangeValueForKey:
}
内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:
)
2、如何手动触发KVO?
答: 手动调用 willChangeValueForKey:
和didChangeValueForKey:
方法即可。
3、直接修改成员变量会触发KVO么?
答: 不会触发 KVO。
二、KVC
KVC 的全称是 Key-Value Coding,俗称“键值编码”,可以通过一个 key 来访问某个属性
常见的 API 有:
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
setValue:forKey: 赋值的原理
赋值顺序: setKey:、_setKey:
,
没找到的话,看 accessInstanceVariablesDirectly
是否允许直接访问成员变量,允许的话,按 _key、_isKey、key、isKey
查找。找到了直接赋值,找不到就调用 valueForUndefineKey:
方法并抛出 NSUnknownKeyException
异常。
如下图:
valueForKey: 取值的原理
取值顺序: getKey、key、isKey、_key
。
没找到的话,看 accessInstanceVariablesDirectly
是否允许直接访问成员变量,允许的话,按 _key、_isKey、key、isKey
查找。找到了直接赋值,找不到就调用 valueForUndefineKey:
方法并抛出 NSUnknownKeyException
异常。
KVC 相关面试题:
1、通过KVC修改属性会触发KVO么?
答:会触发 KVO。(不管有没有触发set方法都会触发KVO,没有set方法的话kvc修改的是成员变量,内部调用了willChangeValueForKey:
和 didChangeValueForKey:
,,所以也会触发)
验证方法:
在 MyPerson.h 中只声明成员变量 age,不写属性 age,也不去重写 setAge:
方法。
@interface MyPerson : NSObject {
@public
int age;
int isAge;
int _isAge;
int _age;
}
在 MyPerson.m 中,我们自己重写下 willChangeValueForKey:
和 didChangeValueForKey:
方法,在里面打印,并且重写 accessInstanceVariablesDirectly
返回 YES,让 KVC 可直接访问成员变量。
- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey - %@", key);
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValueForKey - begin - %@", key);
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end - %@", key);
}
// 默认的返回值就是YES
+ (BOOL)accessInstanceVariablesDirectly {
return YES;
}
我们在外部用 kvc 修改 age 的值,
[person setValue:@10 forKey:@"age"];
可以发现 KVC 设置 age 成功了,并且打印了willChangeValueForKey:
和 didChangeValueForKey:
方法中的输出。
2、KVC的赋值和取值过程是怎样的?原理是什么?
赋值顺序: setKey:、_setKey:
,
没找到的话,看 accessInstanceVariablesDirectly
是否允许直接访问成员变量,允许的话,按 _key、_isKey、key、isKey
查找。找到了直接赋值,找不到就调用 valueForUndefineKey:
方法并抛出 NSUnknownKeyException
异常。
取值顺序: getKey、key、isKey、_key
。
没找到的话,看 accessInstanceVariablesDirectly
是否允许直接访问成员变量,允许的话,按 _key、_isKey、key、isKey
查找。找到了直接赋值,找不到就调用 valueForUndefineKey:
方法并抛出 NSUnknownKeyException
异常。
以上的总结参考了并部分摘抄了以下文章,非常感谢以下作者的分享!:
小马哥-李明杰的《KVO》课程
转载请备注原文出处,不得用于商业传播——凡几多