一、KVO
问题
-
iOS
用什么方式实现对一个对象的KVO
?(KVO
的本质是什么?) - 如何手动触发
KVO
?
1. KVO
使用
KVO
的全称Key-Value Observing
,俗称“键值监听”,可以用于监听某个对象属性值的改变。通过方法(void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
给对象添加监听
Person *p1 = [[Person alloc] init];
Person *p2 = [[Person alloc] init];
p1.age = 2;
p2.age = 5;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
p1.age = 10;
p2.age = 20;
当对象某个属性的值发生了改变之后会回调方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
}
监听到对象的age属性改变了,{
kind = 1;
new = 10;
old = 2;
}
上述代码中可以看出,在添加监听之后,age
属性的值在发生改变时,就会通知到监听者,执行监听者的observeValueForKeyPath
方法。
记得在合适的时机移除对对象的监听(void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
2. KVO
实现原理
2.1 分析
通过上述代码我们发现,一旦age
属性的值发生改变时,就会通知到监听者(观察者),并且我们知道赋值操作都是调用set
方法,我们可以来到Persn
类中重写age
的set
方法,观察是否是KVO
在set
方法内部做了一些操作来通知监听者。
我们发现即使重写了set
方法,p1
对象和p2
对象调用同样的set
方法,但是我们发现p1
除了调用set
方法之外还会另外执行监听器的observeValueForKeyPath
方法。
说明KVO
在运行时会对p1
对象做了一些改变。相当于在程序运行过程中,对p1
对象做了一些变化,使得p1
对象在调用setage
方法的时候可能做了一些额外的操作,所以问题出在对象本身。
2.2 本质
首先我们对上述代码中添加监听的地方打断点,看观察一下,addObserver
方法对p1
对象做了什么处理?也就是说p1
对象在经过addObserver
方法之后发生了什么改变,我们通过打印isa指针如下所示
// 添加监听之前的isa
(lldb) po p1->isa
Person
(lldb) po p2->isa
Person
// 添加监听之后的isa
(lldb) po p1->isa
NSKVONotifying_Person
(lldb) po p2->isa
Person
我们发现,p1
对象执行过addObserver: forKeyPath:
操作之后,p1
实例对象的isa
指针由之前的指向类对象Person
变为指向NSKVONotifyin_Person
类对象,而p2
对象没有任何改变。也就是说一旦p1
对象添加了KVO
监听以后,其isa
指针就会发生变化,因此类对象里面的的set
方法的执行效果就不一样了。
那么我们先来观察p2
对象在内容中是如何存储的,然后对比p2
来观察p1
。
p2
在调用setage
方法的时候,首先会通过p2
实例对象中的isa
指针找到Person
类对象,然后在类对象中找到setage
方法。然后找到方法对应的实现。如下图所示
2.3 添加KVO
之后的实例对象isa
指向
p1
实例对象的isa
指针在经过KVO
监听之后已经指向了NSKVONotifyin_Person
类对象。
NSKVONotifyin_Person
其实是Person
的子类,那么也就是说其super_class
指针是指向Person
类对象的,NSKVONotifyin_Person
类是runtime
在运行时生成的。
那么p1
实例对象在调用setage
实例方法的时候,会根据p1
的isa
找到NSKVONotifyin_Person
类对象,在NSKVONotifyin_Person中
找setage的方法及实现。
2.4 NSKVONotifyin_xxx
内部做的操作
NSKVONotifyin_Person
中的setage
方法中其实调用了Fundation
框架中C
语言函数 _NSsetIntValueAndNotify
。
_NSsetIntValueAndNotify
函数内部做的操作相当于:
- 调用
willChangeValueForKey
将要改变方法,类似于[self willChangeValueForKey:]
, - 调用父类的
setage
方法对成员变量赋值,类似于[super setage:]
- 最后调用
didChangeValueForKey
已经改变方法,类似于[self didChangeValueForKey:]
didChangeValueForKey
中会调用监听者的监听方法,最终来到监听者的observeValueForKeyPath
方法中。
3. 验证KVO
的内部实现
我们已经验证了,在执行添加监听的方法时,会将isa
指针指向一个通过runtime
创建的Person
的子类NSKVONotifyin_Person
的类对象。
另外我们可以通过打印方法实现的地址来看一下p1
和p2
的setage:
的方法实现的地址在添加KVO
前后有什么变化。
// 通过methodForSelector找到方法实现的地址
NSLog(@"添加KVO监听之前 p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
// self 监听 p1的 age属性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"添加KVO监听之后 p1 = %p, p2 = %p", [p1 methodForSelector:@selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
进入lldb
:
2020-01-09 16:27:09.786420+0800 KVO的本质[98033:1408550] 添加KVO监听之前 p1 = 0x109df9040, p2 = 0x109df9040
2020-01-09 16:27:09.786912+0800 KVO的本质[98033:1408550] 添加KVO监听之后 p1 = 0x7fff2564d626, p2 = 0x109df9040
(lldb) p (IMP)0x109df9040
(IMP) $0 = 0x0000000109df9040 (KVO的本质`-[Person setAge:] at Person.h:15)
(lldb) p (IMP)0x7fff2564d626
(IMP) $1 = 0x00007fff2564d626 (Foundation`_NSSetIntValueAndNotify)
(lldb)
我们发现在添加KVO
监听之前,p1
和p2
的setAge:
方法实现的地址相同,而经过KVO
之后,p1
的setAge:
方法实现的地址发生了变化。
我们通过打印方法实现来看一下前后的变化发现,确实如我们上面所讲的一样,p1
的setAge:
方法的实现由Person
类对象中的setAge:
方法转换为了C
语言Foundation
框架的_NSsetIntValueAndNotify
函数。
Foundation
框架中会根据属性的类型,调用不同的方法。例如我们之前定义的int
类型的age
属性,那么我们看到Foundation
框架中调用的_NSsetIntValueAndNotify
函数。那么我们把age
的属性类型变为double
重新打印一遍:
2020-01-09 16:30:30.633548+0800 KVO的本质[98184:1412183] 添加KVO监听之前 p1 = 0x10567c010, p2 = 0x10567c010
2020-01-09 16:30:30.633898+0800 KVO的本质[98184:1412183] 添加KVO监听之后 p1 = 0x7fff2564d3a8, p2 = 0x10567c010
(lldb) p (IMP)0x10567c010
(IMP) $0 = 0x000000010567c010 (KVO的本质`-[Person setAge:] at Person.h:15)
(lldb) p (IMP)0x7fff2564d3a8
(IMP) $1 = 0x00007fff2564d3a8 (Foundation`_NSSetDoubleValueAndNotify)
(lldb)
我们发现调用的函数变为了_NSSetDoubleValueAndNotify
,那么这说明Foundation
框架中有许多此类型的函数,通过属性的不同类型调用不同的函数。
那么我们可以推测Foundation
框架中还有很多例如_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify
等等函数。
我们可以找到Foundation
框架文件,通过命令行查询关键字找到相关函数
4. Foundation
框架的_NSSet*ValueAndNotify
函数的内部逻辑
我们在Person
类中重写willChangeValueForKey:
和didChangeValueForKey:
方法,模拟他们的实现。
- (void)setAge:(int)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");
}
再次运行来查看didChangeValueForKey:
的方法内运行过程,通过打印的顺序可以看到,确实在didChangeValueForKey:
方法内部已经调用了observer
的observeValueForKeyPath:ofObject:change:context:
方法。
2020-01-09 17:07:17.044803+0800 KVO的本质[890:1450834] willChangeValueForKey: - begin
2020-01-09 17:07:17.044941+0800 KVO的本质[890:1450834] willChangeValueForKey: - end
2020-01-09 17:07:17.045039+0800 KVO的本质[890:1450834] 调用了setAge
2020-01-09 17:07:17.045133+0800 KVO的本质[890:1450834] didChangeValueForKey: - begin
2020-01-09 17:07:17.045323+0800 KVO的本质[890:1450834] 监听到对象的age属性改变了,{
kind = 1;
new = 249421248;
old = 249421248;
}
2020-01-09 17:07:17.045423+0800 KVO的本质[890:1450834] didChangeValueForKey: - end
5. NSKVONotifyin_Person
类对象存储的实例方法
NSKVONotifyin_Person
作为Person
的子类,其super_class
指针指向Person
类对象,并且NSKVONotifyin_Person
内部一定对setAge:
方法做了单独的实现,那么NSKVONotifyin_Person
同Person
类的差别可能就在于其存储的实例方法及方法实现不同。
我们通过runtime
分别打印Person
类对象和NSKVONotifyin_Person
类对象内存储的实例方法
// 通过runtime打印类对象的方法名
- (void)printMethods: (Class)cls {
unsigned int count;
Method *methods = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [NSMutableString string];
[methodNames appendFormat:@"%@类的方法 - ", cls];
for (int i = 0 ; i < count; i++) {
Method method = methods[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methodNames appendString: methodName];
[methodNames appendString:@","];
}
NSLog(@"%@",methodNames);
free(methods);
}
// 通过methodForSelector找到方法实现的地址
NSLog(@"添加KVO监听之前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
// self 监听 p1的 age属性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"添加KVO监听之后 - p1 = %p, p2 = %p", [p1 methodForSelector:@selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
[self printMethods: object_getClass(p2)];
[self printMethods: object_getClass(p1)];
结果
2020-01-09 16:48:16.718818+0800 KVO的本质[99651:1433359] Person类的方法 - age,setAge:,
2020-01-09 16:48:16.718958+0800 KVO的本质[99651:1433359] NSKVONotifying_Person类的方法 - setAge:,class,dealloc,_isKVOA,
通过上述代码我们发现NSKVONotifyin_Person
中有4个对象方法。分别为setAge:
、class
、 dealloc
、 _isKVOA
,那么至此我们可以画出NSKVONotifyin_Person
的内存结构以及方法调用顺序。
这里NSKVONotifyin_Person
重写class
方法是为了隐藏NSKVONotifyin_Person
不被外界所看到。我们在p1
添加过KVO
监听之后,分别打印p1
和p2
对象的class
方法可以发现他们都返回Person
。
NSLog(@"%@,%@",[p1 class],[p2 class]);
// Person,Person
如果NSKVONotifyin_Person
不重写class
方法,那么当对象要调用class
对象方法的时候就会一直向上找来到NSObject
,而NSObject
的class
的实现大致为返回自己真实isa
指向的类,就是p1
的isa
指向的类那么打印出来的类就是NSKVONotifyin_Person
.
但是官方不希望将NSKVONotifyin_Person
类暴露出来,并且不希望我们知道NSKVONotifyin_Person
内部实现,所以在内部重写了class类,直接返回Person类,所以外界在调用p1
的class
对象方法时是Person
类。这样p1
给外界的感还是Person
类,并不知道NSKVONotifyin_Person
子类的存在。
那么我们可以猜测NSKVONotifyin_Person
内重写的class
方法内部实现大致为:
- (Class) class {
// 得到自己的类对象,再找到类对象父类
return class_getSuperclass(object_getClass(self));
}
6. 手动触发KVO
通过手动触发对象的didChangeValueForKey:
方法可以触发KVO:
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"];
[p1 removeObserver:self forKeyPath:@"age"];
打印结果:
监听到对象的age属性改变了,{
kind = 1;
new = 2;
old = 2;
}
通过打印我们可以发现,didChangeValueForKey
方法内部成功调用了observeValueForKeyPath:ofObject:change:context:
,并且age
的值并没有发生改变。
7. 问题:
iOS
用什么方式实现对一个对象的KVO
?(KVO
的本质是什么?)
当一个对象使用了KVO
监听,iOS
系统会修改这个对象的isa
指针,改为指向一个全新的通过Runtime
动态创建的子类,子类拥有自己的set
方法实现,set
方法实现内部会调用Foundation
的_NSSet*ValueAndNotify
方法。
_NSSet*ValueAndNotify
方法内部顺序调用willChangeValueForKey
方法、原来父类的setter
方法实现、didChangeValueForKey
方法,而didChangeValueForKey
方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:
监听方法。
如何手动触发KVO
?
被监听的属性的值被修改时,就会自动触发KVO
。如果想要手动触发KVO
,则需要我们自己调用willChangeValueForKey
和didChangeValueForKey
方法即可在不改变属性值的情况下手动触发KVO
,并且这两个方法缺一不可。
直接修改成员变量的值会触发KVO
吗?
KVO
是重写了set
方法,直接修改成员变量不会触发set
方法,所以不会触发KVO
。
person->_age = 2;
二、KVC
问题
- 通过
KVC
修改属性会触发KVO
吗? -
KVC
的赋值和取值过程是怎么样的?原理是什么?
1. 使用
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;
Person *person = [[Person alloc]init];
[person setValue:@10 forKey:@"age"];
// person对象的cat属性的weight属性
person.cat = [[Cat alloc] init];
[person setValue:@20 forKeyPath:@"cat.weight"];
NSLog(@"%@",[person valueForKey:@"age"]);
NSLog(@"%@",[person valueForKeyPath:@"cat.weight"]);
2. setValue:forKey:
设值的原理
2.1 KVC
会触发KVO
- 通过访问属性
Observer *observer = [[Observer alloc]init];
Person *person = [[Person alloc]init];
[person addObserver:observer forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
// 通过KVC修改age属性的值
[person setValue:@10 forKey:@"age"];
[person removeObserver:observer forKeyPath:@"age"];
我们发现是触发了KVO
的:
2020-04-10 21:34:18.284166+0800 KVC的本质[29149:8625107] observeValueForKeyPath - {
kind = 1;
new = 10;
old = 0;
}
需要注意的是这里Person
类是有属性age``的`,所以系统会自动生成
set```方法。
2.2 原理
通过KVC
的方式赋值的时候其实通过访问属性的set
方法和访问成员变量来达到设值的。
- 首先会调用属性的
set
方法来赋值:
- (void)setAge:(int)age {
NSLog(@"setAge: %d", _age);
}
- 如果没有找到属性的
set
方法,那么会来到_setAge
方法来赋值:
- (void)_setAge:(int)age {
NSLog(@"_setAge: %d", _age);
}
- 如果没有找到
_setAge
方法,那么会来到accessInstanceVariablesDirectly
方法,来询问是否可以访问成员变量来赋值:
+ (BOOL)accessInstanceVariablesDirectly {
return YES;
}
如果返回NO
,那么会抛出异常,如果返回YES
,则表示可以通过访问成员变量来赋值。
- 访问成员变量会按照
_age
、_isAge
、age
、isAge
的顺序来逐个访问赋值,如果这4个成员变量都没有找到,就抛出异常。
@interface Person : NSObject
{
@public
int _age;
int _isAge;
int age;
int isAge;
}
2.3 KVC
会触发KVO
- 通过访问成员变量
现在我们删除Person
的属性,添加成员变量_age
@interface Person : NSObject
{
@public
int _age;
}
我们发现通过KVC
改变属性照样可以触发KVO
KVC的本质[30832:8656264] observeValueForKeyPath - {
kind = 1;
new = 10;
old = 0;
}
我们来重写Person
的willChangeValueForKey
和didChangeValueForKey
方法:
- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key {
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey");
}
KVC的本质[30911:8657993] willChangeValueForKey
2020-04-10 22:14:44.426343+0800 KVC的本质[30911:8657993] observeValueForKeyPath - {
kind = 1;
new = 10;
old = 0;
}
2020-04-10 22:14:44.426514+0800 KVC的本质[30911:8657993] didChangeValueForKey
我们发现KVC
通过访问成员变量来赋值的时候,其内部会主动触发KVO
。但是我们自己访问成员变量不会触发KVO
。原因就是通过KVC
访问成员变量的时候,系统会主动触发KVO
,相当于:
[person willChangeValueForKey:@"age"];
person->_age = 10;
[person didChangeValueForKey:@"age"];
3. valueForKey:
取值的原理
和赋值一样的,也是按顺序访问方法和成员变量获得值:
- 通过
getAge
方法取值
- (int)getAge {
return 10;
}
- 如果没有
getAge
方法,通过age
方法取值
- (int)age {
return 11;
}
- 如果没有
age
方法,通过isAge
方法取值
- (int)isAge {
return 12;
}
- 如果没有
isAge
方法,通过_age
方法取值
- (int)_age {
return 13;
}
- 如果以上方法都没有找到,那么会通过
accessInstanceVariablesDirectly
方法的返回值来确定是否可以访问成员变量取值
_age
_isAge
age
isAge