iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?) 如何手动触发KVObash
首先须要了解KVO基本使用,KVO的全称 Key-Value Observing
,俗称“键值监听”,能够用于监听某个对象属性值的改变。app
- (void)viewDidLoad {
[super viewDidLoad];
Person *p1 = [[Person alloc] init];
Person *p2 = [[Person alloc] init];
p1.age = 1;
p1.age = 2;
p2.age = 2;
// self 监听 p1的 age属性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
p1.age = 10;
[p1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@改变了%@", object, keyPath,change);
}
// 打印内容
监听到<Person: 0x604000205460>的age改变了{
kind = 1;
new = 10;
old = 2;
}
复制代码
上述代码中能够看出,在添加监听以后,age属性的值在发生改变时,就会通知到监听者,执行监听者的observeValueForKeyPath
方法。框架
经过上述代码咱们发现,一旦age
属性的值发生改变时,就会通知到监听者,而且咱们知道赋值操做都是调用set
方法,咱们能够来到Person
类中重写age
的set
方法,观察是不是KVO
在set
方法内部作了一些操做来通知监听者。函数
咱们发现即便重写了set
方法,p1对象和p2对象调用一样的set方法,可是咱们发现p1除了调用set方法以外还会另外执行监听器的observeValueForKeyPath
方法。ui
说明KVO
在运行时获取对p1对象作了一些改变。至关于在程序运行过程当中,对p1对象作了一些变化,使得p1对象在调用setAge:
方法的时候可能作了一些额外的操做,因此问题出在对象身上,两个对象在内存中确定不同,两个对象可能本质上并不同。接下来来探索KVO内部是怎么实现的。spa
首先咱们对上述代码中添加监听的地方打断点,看观察一下,addObserver方法对p1对象作了什么处理?也就是说p1对象在通过addObserver方法以后发生了什么改变,咱们经过打印isa指针以下图所示命令行
经过上图咱们发现,p1
对象执行过addObserver
操做以后,p1
对象的isa
指针由以前的指向类对象Person
变为指向NSKVONotifyin_Person
类对象,而p2对象没有任何改变。也就是说一旦p1对象添加了KVO
监听之后,其isa
指针就会发生变化,所以set
方法的执行效果就不同了。指针
那么咱们先来观察p2
对象在内容中是如何存储的,而后对比p2
来观察p1
。 首先咱们知道,p2
在调用setAge:
方法的时候,首先会经过p2
对象中的isa
指针找到Person
类对象,而后在类对象中找到setAge:
方法。而后找到方法对应的实现。以下图所示code
可是刚才咱们发现p1
对象的isa
指针在通过KVO
监听以后已经指向了NSKVONotifyin_Person
类对象,NSKVONotifyin_Person
实际上是Person
的子类,那么也就是说其superclass
指针是指向Person
类对象的,NSKVONotifyin_Person
是runtime
在运行时生成的。那么p1对象在调用setAge:
方法的时候,确定会根据p1的isa
找到NSKVONotifyin_Person
,在 NSKVONotifyin_Person
中找setAge:
的方法及实现。orm
通过查阅资料咱们能够了解到。 NSKVONotifyin_Person
中的setAge
方法中其实调用了Fundation
框架中C语言函数 _NSsetIntValueAndNotify
,_NSsetIntValueAndNotify
内部作的操做至关于,首先调用willChangeValueForKey
将要改变方法,以后调用父类的setAge
方法对成员变量赋值,最后调用didChangeValueForKey
已经改变方法。didChangeValueForKey
中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath
方法中。
首先通过以前打断点打印isa
指针,咱们已经验证了,在执行添加监听的方法时,会将isa
指针指向一个经过runtime
建立的Person
的子类NSKVONotifyin_Person
。 另外咱们能够经过打印方法实现的地址来看一下p1
和p2
的setAge:
的方法实现的地址在添加KVO先后有什么变化
// 经过methodForSelector找到方法实现的地址
NSLog(@"添加KVO监听以前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
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:)]);
复制代码
咱们发如今添加KVO
监听以前,p1
和p2
的setAge:
方法实现的地址相同,而通过KVO
监听以后,p1
的setAge
方法实现的地址发生了变化,咱们经过打印方法实现来看一下先后的变化发现,确实如咱们上面所讲的同样,p1
的setAge
方法的实现由Person
类方法中的setAge方法转换为了C语言的Foundation
框架的_NSsetIntValueAndNotify
函数。
Foundation
框架中会根据属性的类型,调用不一样的方法。例如咱们以前定义的int
类型的age
属性,那么咱们看到Foundation
框架中调用的_NSsetIntValueAndNotify
函数。那么咱们把age
的属性类型变为double
从新打印一遍
咱们发现调用的函数变为了_NSSetDoubleValueAndNotify
,那么这说明Foundation
框架中有许多此类型的函数,经过属性的不一样类型调用不一样的函数。 那么咱们能够推测Foundation
框架中还有不少例如_NSSetBoolValueAndNotify
、_NSSetCharValueAndNotify
、_NSSetFloatValueAndNotify
、_NSSetLongValueAndNotify
等等函数。 咱们能够找到Foundation
框架文件,经过命令行查询关键字找到相关函数
首先咱们知道,NSKVONotifyin_Person
做为Person
的子类,其superclass
指针指向Person
类,而且NSKVONotifyin_Person
内部必定对setAge
方法作了单独的实现,那么NSKVONotifyin_Person
同Person
类的差异可能就在于其内存储的对象方法及实现不一样。 咱们经过runtime
分别打印Person
类对象和NSKVONotifyin_Person
类对象内存储的对象方法
- (void)viewDidLoad {
[super viewDidLoad];
Person *p1 = [[Person alloc] init];
p1.age = 1.0;
Person *p2 = [[Person alloc] init];
p1.age = 2.0;
// self 监听 p1的 age属性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
[self printMethods: object_getClass(p2)];
[self printMethods: object_getClass(p1)];
[p1 removeObserver:self forKeyPath:@"age"];
}
- (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);
}
复制代码
上述打印内容以下
经过上述代码咱们发现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
,可是apple
不但愿将NSKVONotifyin_Person
类暴露出来,而且不但愿咱们知道NSKVONotifyin_Person
内部实现,因此在内部重写了class
类,直接返回Person
类,因此外界在调用p1
的class
对象方法时,是Person
类。这样p1给外界的感受p1仍是Person
类,并不知道NSKVONotifyin_Person
子类的存在。
那么咱们能够猜想NSKVONotifyin_Person
内重写的class
内部实现大体为
- (Class) class {
// 获得类对象,在找到类对象父类
return class_getSuperclass(object_getClass(self));
}
复制代码
咱们在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
方法。
** 当一个对象使用了KVO
监听,iOS系统会修改这个对象的isa
指针,改成指向一个全新的经过Runtime
动态建立的子类,子类拥有本身的set
方法实现,set方法实现内部会顺序调用willChangeValueForKey
方法、原来的setter
方法实现、didChangeValueForKey
方法,而didChangeValueForKey
方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context
监听方法。**
被监听的属性的值被修改时,就会自动触发KVO。若是想要手动触发KVO
,则须要咱们本身调用willChangeValueForKey
和didChangeValueForKey
方法便可在不改变属性值的状况下手动触发KVO,而且这两个方法缺一不可。
经过如下代码能够验证
Person *p1 = [[Person alloc] init];
p1.age = 1.0;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"];
[p1 removeObserver:self forKeyPath:@"age"];
复制代码
经过打印咱们能够发现,didChangeValueForKey
方法内部成功调用了observeValueForKeyPath:ofObject:change:context:
,而且age的值并无发生改变。