探索KVO原理
有这么一个示例:
- Person类,有一个实例变量nickName,属性name,对象方法run,work:
// Person.h
@interface Person : NSObject
{
NSString *nickName;
}
@property (nonatomic, copy) NSString *name;
- (void)run;
- (void)work;
@end
// Person.m 实现方法
- (void)work {
}
- (void)run {
}
- Student类继承自Person类:
// Student.h 什么也没做
@interface Student : Person
@end
// Student.m 实现继承来的run方法
- (void)run {
}
- 有两个工具方法,遍历类的方法 和 遍历类及子类
#pragma mark - 遍历方法
- (void)printClassAllMethod:(Class)cls {
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
NSMutableArray *mArray = [NSMutableArray array];
for (int i = 0; i < count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@ -- %p", NSStringFromSelector(sel), imp);
[mArray addObject:NSStringFromSelector(sel)];
}
NSLog(@"allMethod = %@", mArray);
}
#pragma mark - 遍历类及子类
- (void)printClasses:(Class)cls {
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组,其中包含给定的对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class *classes = (Class *)malloc(sizeof(Class) * count);
objc_getClassList(classes, count);
for (int i = 0; i < count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
- 调用工具方法打印Person 和 Student的所有方法及类和子类
[self printClassAllMethod:[Person class]];
[self printClassAllMethod:[Student class]];
[self printClasses:[Person class]];
[self printClasses:[Student class]];
- 打印结果
Person -- ClassAllMethod
2019-11-12 22:07:27.189171+0800 KVO[29094:3896664] allMethod = (
".cxx_destruct",
name,
"setName:",
run,
work
)
Student -- ClassAllMethod
2019-11-12 22:07:27.189791+0800 KVO[29094:3896664] allMethod = (
run
)
Person -- Classes
2019-11-12 22:07:27.217213+0800 KVO[29094:3896664] classes = (
Person,
Student
)
Student -- Classes
2019-11-12 22:07:27.222664+0800 KVO[29094:3896664] classes = (
Student
)
- Persor类的所有方法,可以看到除了声明的run,work,还有系统自动生成的属性的setter和getter方法,实例变量nickName没有生成setter和getter方法
- Student类只有继承来的run,且方法实现地址imp和父类不一样
- Person类除了自身还有子类Student
- Student只有自己
》》》和我们预期的完全一样《《《
- 添加一个观察者
self.person = [[Person alloc] init];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:nil];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
[self printClasses:[Person class]];
打印发现此时的Person类及子类,多了一个NSKVONotifying_Person类,说明NSKVONotifying_Person是动态生成的Person的子类
2019-11-12 22:28:32.530026+0800 KVO[29303:3917560] classes = (
Person,
"NSKVONotifying_Person",
Student
)
我们断点观察者注册前后,发现实例对象self.person真正指向的类从Person变为了NSKVONotifying_Person,真是神奇。

- 既然有NSKVONotifying_Person这个类,那么把这个类的所有方法打印出来瞧一瞧
[self printClassAllMethod:NSClassFromString(@"NSKVONotifying_Person")];
打印结果:
2019-11-12 22:39:27.756694+0800 KVO[29389:3928692] setName: -- 0x7fff2564cec6
2019-11-12 22:39:27.756979+0800 KVO[29389:3928692] class -- 0x7fff2564b989
2019-11-12 22:39:27.757287+0800 KVO[29389:3928692] dealloc -- 0x7fff2564b6ee
2019-11-12 22:39:27.757561+0800 KVO[29389:3928692] _isKVOA -- 0x7fff2564b6e6
2019-11-12 22:39:27.757720+0800 KVO[29389:3928692] allMethod = (
"setName:",
class,
dealloc,
"_isKVOA"
)
观察前
setName: -- 0x1038bf7d0
观察后
setName: -- 0x7fff2564cec6
哇,看到重点了,除了这是KVO类的标记和dealloc方法,看到了setName方法,再结合观察前后setName方法对应的方法实现IMP地址的不同,可以推断NSKVONotifying_Person重写了setter方法,因而在对象上对setter的调用就会调用已重写的setter,从而激活键值通知机制。
总结KVO原理:

-
Apple使用了
isa
混写(isa-swizzling)来实现KVO
。当观察对象为类A
的属性时,KVO
机制动态创建一个新的名为:NSKVONotifying_A
的新类,该类继承自A
的本类,且KVO
为NSKVONotifying_A
重写观察属性的setter
方法,setter
方法会负责在调用原setter方法之前和之后,通知所有观察对象属性值的更改情况。 -
NSKVONotifying_A
类剖析:在这个过程,被观察对象的isa
指针从指向原来的A
类,被KVO
机制修改为指向系统新创建的子类NSKVONotifying_A
类,来实现当前类属性值改变的监听。 -
所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对
KVO
的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”
的类,就会发现系统运行到注册KVO
的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A
的中间类,并指向这个中间类了。 -
isa
指针的作用:每个对象都有isa
指针,指向该对象的类,它告诉runtime
系统这个对象的类时什么。所以对象注册为观察者时,isa
指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。因而在该对象上对setter
的调用就会调用已重写的setter
,从而激活键值通知机制。 -
子类
setter
方法剖析:KVO
的键值观察通知依赖于NSObject
的两个方法:willChangeValueForKey:
和didChangeValueForKey:
,在存取数值的前后分别调用2个方法:被观察属性发生改变之前,willChangeValueForKey:
被调用,通知系统该keyPath
的属性值即将变更;在改变发生之后,didChangeValueForKey:
被调用,通知系统该keyPath
的属性值已经变更;之后,observeValueForKey:ofObject:change:context:
也会被调用。且重写观察属性的setter
方法这种继承方式的注入是在运行时而不是编译时实现的。
示例代码可以在这里下载:
https://github.com/SPIREJ/KVO
网友评论