一. KVO
1. KVO的基本使用
KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变
添加监听:
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self.person1 addObserver:self forKeyPath:@"height" options:options context:@"456"];
值改变:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 20;
self.person2.age = 20;
self.person1.height = 30;
self.person2.height = 30;
}
监听改变:
//context:@"123" 作用:在添加监听的时候传入,传到下面这个方法里面
/**
当监听对象的属性值发生改变时,就会调用
@param keyPath 监听的KeyPath
@param object 被监听的对象
@param change 改变
@param context 监听时传入的context
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
移除监听:
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
[self.person1 removeObserver:self forKeyPath:@"height"];
}
2. KVO底层是怎么实现的
为了探究KVO的底层是怎么实现的,我们创建person1和person2,其中person1添加监听,person2不添加监听,代码如下:
#import "ViewController.h"
#import "MJPerson.h"
@interface ViewController ()
@property (strong, nonatomic) MJPerson *person1;
@property (strong, nonatomic) MJPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MJPerson alloc] init];
self.person2.age = 2;
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// self.person1.age = 21;
// self.person2.age = 22;
// NSKVONotifying_MJPerson是使用Runtime动态创建的一个类,是MJPerson的子类
//如果你自己写了这个类,就会报动态生成失败
//KVO效率没代理高,因为代理是直接调用,KVO还要动态生成一个类
// self.person1.isa == NSKVONotifying_MJPerson
[self.person1 setAge:21];
// self.person2.isa = MJPerson
[self.person2 setAge:22];
}
- (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);
}
@end
打断点,分别po它们的isa
(lldb) po self.person1.isa
NSKVONotifying_MJPerson
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) po self.person2.isa
MJPerson
Fix-it applied, fixed expression was:
self.person2->isa
(lldb)
可以发现,person1添加监听后isa是NSKVONotifying_MJPerson,person2不添加监听isa还是MJPerson。
- 其实NSKVONotifying_MJPerson是系统利用Runtime动态创建的一个类,是MJPerson的子类。
- 如果你自己写了这个类,就会报动态生成失败。
- KVO效率没代理高,因为代理是直接调用,KVO还要动态生成一个类。
既然NSKVONotifying_MJPerson也是一个类,那么它肯定也有自己的isa和superclass,未使用KVO和使用KVO,实例对象和类对象内存结构如下:
解释:
① 当person2不添加监听的时候,值改变,会通过person2的isa找到MJPerson,然后再找到MJPerson里面的setAge方法调用,完成。
② 当person1添加监听的时候,值改变,会通过person1的isa找到NSKVONotifying_MJPerson,然后调用NSKVONotifying_MJPerson的setAge方法(方法内部会调用Foundation框架的_NSSetIntValueAndNotify),不会调用MJPerson的setAge方法了。
由于无法查看Foundation框架的实现,我们写一些伪代码,来表明添加监听后的方法调用顺序,创建NSKVONotifying_MJPerson类继承于MJPerson。
#import "MJPerson.h"
@interface NSKVONotifying_MJPerson : MJPerson
@end
#import "NSKVONotifying_MJPerson.h"
@implementation NSKVONotifying_MJPerson
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
// 伪代码
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key
{
// 通知监听器,某某属性值发生了改变
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end
总结:
- 使用KVO,系统会使用Runtime动态创建的一个NSKVONotifying_MJPerson类,这个类是MJPerson的子类。
- 添加监听的属性的值改变的时候,会调用NSKVONotifying_MJPerson类的setAge方法,setAge方法里面会调用_NSSetIntValueAndNotify方法,_NSSetIntValueAndNotify里面走如下步骤:
① willChangeValueForKey 将要改变
② setAge(原来的set方法) 真的去改变
③ didChangeValueForKey 已经改变
④ observeValueForKeyPath:ofObject:change:context: 监听到MJPerson的age属性改变了
3. 验证_NSSetIntValueAndNotify内部方法调用流程
验证过程也很简单,重写MJPerson类的三个方法,如下:
- (void)setAge:(int)age
{
_age = age;
NSLog(@"setAge:");
}
- (void)willChangeValueForKey:(NSString *)key
{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
赋值之后运行,打印结果如下:
willChangeValueForKey
setAge:
didChangeValueForKey - begin
监听到<MJPerson: 0x6000017c90f0>的age属性值改变了 - {
kind = 1;
new = 21;
old = 1;
} - 123
didChangeValueForKey - end
4. 验证生成了NSKVONotifying_MJPerson
结论我们知道了,接下来还是用代码验证一下:
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MJPerson 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:)]);
}
如上,我们在添加KVO之前和之后分别打印它的类对象和它的setAge方法实现,结果如下:
person1添加KVO监听之前 - MJPerson MJPerson
person1添加KVO监听之前 - 0x10ede8590 0x10ede8590
person1添加KVO监听之后 - NSKVONotifying_MJPerson MJPerson
person1添加KVO监听之后 - 0x10f143216 0x10ede8590
可以发现:
person1添加KVO之前,person1和person2的类对象和setAge方法都是一样的。
person1添加KVO之后,person1的isa指向的类对象变成了NSKVONotifying_MJPerson,setAge方法地址也变了,person2什么都没变。
接下来,我们通过p (IMP)指令打印某个地址对应的实现:
(lldb) p (IMP)0x10ede8590
(IMP) $0 = 0x000000010ede8590 (Interview01`-[MJPerson setAge:] at MJPerson.m:13)
(lldb) p (IMP)0x10f143216
(IMP) $1 = 0x000000010f143216 (Foundation`_NSSetIntValueAndNotify)
(lldb)
打印发现,添加KVO之后果然调用的是Foundation框架下的_NSSetIntValueAndNotify函数,说明我们上面的结论是正确的。
5. NSKVONotifying_MJPerson类对象的isa指向哪里?
下面还有最后一个问题,NSKVONotifying_MJPerson类对象的isa指向哪里呢?这个验证起来也很简单,用如下代码打印:
//获取类对象
NSLog(@"类对象 - %p %p",
object_getClass(self.person1), // 相当于获取person1的类对象(self.person1.isa)
object_getClass(self.person2)); // 相当于获取erson2的类对象(self.person2.isa)
//获取元类对象
NSLog(@"元类对象 - %p %p",
object_getClass(object_getClass(self.person1)), // 相当于获取person1类对象的元类对象(self.person1.isa.isa)
object_getClass(object_getClass(self.person2))); // 相当于获取相当于获取person1类对象的元类对象(self.person2.isa.isa)
打印结果如下;
类对象 - 0x6000008f0090 0x100d91158
元类对象 - 0x6000008f1050 0x100d91180
可以发现person1和person2的类对象和元类对象的地址都不一样,说明他们是不同的类。这和我们以前的结论一致,所以NSKVONotifying_MJPerson类对象的isa也是指向它自己的元类对象。
注意:当属性是int类型的时候调用的是_NSSetIntValueAndNotify方法,当属性是float类型的时候调用的是_NSSetFloatValueAndNotify方法,其他类比_NSSetObjectValueAndNotify,_NSSetLongValueAndNotify等等......
6. 为什么重写class、dealloc、isKVO方法
在第三张图中我们可以看出,创建NSKVONotifying_MJPerson之后会重写setAge、class、dealloc、isKVO 这四个方法,setAge方法我们知道为什么重写,但是为什么要重写后面三个方法呢?
首先我们先验证NSKVONotifying_MJPerson的确有这四个方法:
//获取一个类里面所有的方法
- (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:@", "];
}
//c语言中,如果数组是create或者copy出来的要free OC中ARC不用管
// 释放
free(methodList);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MJPerson alloc] init];
self.person2.age = 2;
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self printMethodNamesOfClass:object_getClass(self.person1)];
[self printMethodNamesOfClass:object_getClass(self.person2)];
}
打印结果如下:
打印结果:
NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,
MJPerson setAge:, age,
由打印结果可知:
NSKVONotifying_MJPerson里面的确有setAge:、class、dealloc、_isKVOA四个方法
MJPerson里面有setAge:、age两个方法
接下来如果想知道为什么要重写class、dealloc、_isKVOA三个方法,我们先打印:
NSLog(@"%@ %@",object_getClass(self.person1),object_getClass(self.person2));
NSLog(@"%@ %@",[self.person1 class],[self.person2 class]);
打印结果:
NSKVONotifying_MJPerson MJPerson
MJPerson MJPerson
在OC对象的分类中,我们知道上面两种方式都可以获取类对象,但是为什么获取的结果不一样呢?
其实,因为NSKVONotifying_MJPerson是内部创建的,不想让用户看到,所以用户调用class方法要把NSKVONotifying_MJPerson转成MJPerson,所以系统才重写了class方法。使用object_getClass函数(RuntimeAPI)获取的就是真实的,不会被转成MJPerson。
如果NSKVONotifying_MJPerson没有实现class方法,最后会调用到NSObject的class方法,会直接返回NSKVONotifying_MJPerson,因为NSObject内部这样实现的:
@implementation NSObject
- (Class)class
{
return object_getClass(self);
}
@end
我们可以写NSKVONotifying_MJPerson的伪代码:
#import "NSKVONotifying_MJPerson.h"
@implementation NSKVONotifying_MJPerson
//NSKVONotifying_MJPerson内部实现了setKey class dealloc isKVO 方法
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
// 屏蔽内部实现,隐藏了NSKVONotifying_MJPerson类的存在
- (Class)class
{
return [MJPerson class];
}
- (void)dealloc
{
// 收尾工作
}
- (BOOL)_isKVOA
{
return YES;
}
@end
7. 面试题
下面我们就可以回答面试题了:
问题一:iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
答:
- 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
- 当修改instance对象的属性时,会先调用这个新子类的setter方法,这个新子类的setter方法内部会调用Foundation的_NSSet*ValueAndNotify函数(内部调用如下方法)
① willChangeValueForKey:
② 父类原来的setter
③ didChangeValueForKey:
④ 内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)
问题二:如何手动触发KVO?(就算没有人修改age值,也想触发监听方法observeValueForKeyPath)
答:手动调用willChangeValueForKey:和didChangeValueForKey:
比如:
[self.person1 willChangeValueForKey:@"age"];
self.person1->_age = 2;
[self.person1 didChangeValueForKey:@"age"];
//didChangeValueForKey内部会判断willChangeValueForKey是否调用,所以两个都要调用
会打印:
2019-04-11 15:34:31.968473+0800 Interview01[16217:3596188] 监听到<MJPerson: 0x6000000180d0>的age属性值改变了 - {
kind = 1;
new = 2;
old = 1;
} - 123
问题三:直接修改成员变量会触发KVO吗?
答:不会触发KVO,因为没调用重写后的set方法。
比如,如下代码,不会触发
self.person1->_age = 2;
二. KVC
1. 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;
KVC的基本使用:
person.age = 10;
NSLog(@"%@", [person valueForKey:@"age"]);
NSLog(@"%@", [person valueForKeyPath:@"cat.weight"]);
NSLog(@"%d", person.age);
//[person setValue:[NSNumber numberWithInt:10] forKey:@"age"];
[person setValue:@10 forKey:@"age"];
person.cat = [[MJCat alloc] init];
[person setValue:@10 forKeyPath:@"cat.weight"];
//setValue:@10 forKeyPath 更强大推荐使用.
NSLog(@"%d", person.age);
2. 通过KVC给属性赋值能触发KVO吗?
能不能试一下就知道了:
MJObserver *observer = [[MJObserver alloc] init];
MJPerson *person = [[MJPerson alloc] init];
// 添加KVO监听
[person addObserver:observer forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
[person setValue:@10 forKey:@"age"];
打印如下:
2019-04-11 15:55:00.568868+0800 Interview01-KVC[16345:3639722] observeValueForKeyPath - {
kind = 1;
new = 10;
old = 0;
}
发现,通过KVC修改age属性会触发KVO。为什么呢?先往下看
接下来看看setValue:forKey:设值原理和valueForKey:取值原理。
3. KVC设值原理
设值原理解释:
1.寻找setAge方法
- (void)setAge:(int)age
{
NSLog(@"setAge: - %d", age);
}
2.寻找_setAge方法
- (void)_setAge:(int)age
{
NSLog(@"_setAge: - %d", age);
}
3.找不到上面两个方法就调用accessInstanceVariablesDirectly问问能不能直接访问成员变量
默认的返回值就是YES
+ (BOOL)accessInstanceVariablesDirectly
{
return YES;
}
4.1 如果返回NO,就调用setValue:forUndefinedKey:并抛出异常NSUnknownKeyException
4.2如果返回YES,会按顺序_key,_isKey,key,isKey赋值,如果四个都找不到就报上面的错
上面我们知道KVC设值会触发KVO,但是如果没有set方法呢?通过验证(验证过程省略)可知,就算没有set方法只有成员变量,通过KVC进行赋值也会触发KVO,可以理解它们是配套使用的。
其实KVC内部调用了下面方法才会触发KVO的,可自行验证。
[person willChangeValueForKey:@"age"];
person->_age = 10;
[person didChangeValueForKey:@"age"];
4. KVC取值原理
KVC取值原理解释,可自行验证:
1.getAge
- (int)getAge
{
return 11;
}
2.age
- (int)age
{
return 12;
}
3.isAge
- (int)isAge
{
return 13;
}
4._age
- (int)_age
{
return 14;
}
5.3.找不到上面四个方法就调用accessInstanceVariablesDirectly问问能不能直接访问成员变量
默认的返回值就是YES
+ (BOOL)accessInstanceVariablesDirectly
{
return YES;
}
5.1 如果返回NO,就调用valueforUndefinedKey:并抛出异常NSUnknownKeyException
5.2如果返回YES,会按顺序_key,_isKey,key,isKey赋值,如果四个都找不到就报上面的错
Demo地址:KVO和KVC原理