KVO,苹果的一种键值监听实现技术。Key-Value Observing Implementation Details
中有如下描述:
Automatic key-value observing is implemented using a technique called isa-swizzling.
我们知道,KVO在注册观察者时会以被观察者为父类生成一个中间类,并将被观察者的isa指向这个中间类,以此来实现观察者模式。
多说无益,上代码。
Demo
@interface MObject : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation MObject
@end
@interface MObserver : NSObject
@end
@implementation MObserver
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([object isKindOfClass:[MObject class]] && [keyPath isEqualToString:@"name"]) {
NSLog(@"new name is %@", change[NSKeyValueChangeNewKey]);
}
}
@end
void kvo_test() {
MObject *obj = [MObject new];
MObserver *observer = [MObserver new];
[obj addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
obj.name = @"test";
[obj removeObserver:observer forKeyPath:@"name"];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
kvo_test();
}
return 0;
}
在obj.name = @"test"
处打上断点后运行程序,控制台执行如下操作:
可以看到,obj的类型被替换成NSKVONotifying_MObject
,这个类的父类才是MObject
。
虽然如此,但obj仅仅做了isa替换,内存布局仍与MObject
相同,之所以替换isa
是为了做键值监听并对外部屏蔽实现细节。也就是说成员变量_name
只存在于MObject
,NSKVONotifying_MObject
中并没有生成这个成员变量,可以用以下代码来验证:
void thIvars(Class cls) {
NSLog(@"=======%@=======", cls);
unsigned int count;
Ivar *ivars = class_copyIvarList(cls, &count);
for (int i = 0; i < count; i++) {
NSLog(@"%s", ivar_getName(ivars[i]));
}
free(ivars);
}
输出如下:
执行obj.name = @"test"
必然调用setter,由于obj的isa
被替换为NSKVONotifying_MObject
,所以会到这个中间类寻找setter,看下是否有这个方法:
void theImplementation(Class cls) {
unsigned int count;
Method *methods = class_copyMethodList(cls, &count);
for (int i = 0; i < count; i++) {
Method method = methods[i];
NSString *name = NSStringFromSelector(method_getName(method));
NSLog(@"%@", name);
if ([name isEqualToString:@"setName:"]) {
NSLog(@"%p", method_getImplementation(method));
}
}
free(methods);
}
void kvo_test() {
MObject *obj = [MObject new];
MObserver *observer = [MObserver new];
[obj addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
theImplementation(object_getClass(obj));
obj.name = @"test";
[obj removeObserver:observer forKeyPath:@"name"];
}
中间类生成了setName:
、class
、dealloc
、_isKVOA
这四个方法,
可见有setter,并且得到对应的函数指针0x7fff3769e119
。当对这个地址下断点时,发现对应的真实函数为_NSSetObjectValueAndNotify
。
对_NSSetObjectValueAndNotify
执行反汇编指令(内容较长,以下展示为部分截图):
这里有一条比较指令,用于判断MObject
是否重写willChangeValueForKey
或者didChangeValueForKey
方法,如果重写则调用重写方法,否则跳转到0x7fff3769e1cd
处。
0x7fff3769e1cd <+180>: movq 0x56bf7834(%rip), %rax ; (void *)0x00007fff9650be40: _NSConcreteStackBlock
此处对应汇编代码如下,显然是个block,KVO的键值观察的默认实现就在这里。此时对willChangeValueForKey:
与didChangeValueForKey:
下断点:
运行程序后并没有匹配到,可见KVO的默认实现没有调用这两个方法,这两个方法只是KVO提供的外部接口用于触发KVO前后再做一些自定义操作。
再来看下默认实现大致调用了哪些方法,在MObserver
中的observeValueForKeyPath:
处打上断点,运行程序,可以看到调用栈如下:
如果新增如下方法:
@implementation MObject
- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey:key];
}
- (void)didChangeValueForKey:(NSString *)key {
[super didChangeValueForKey:key];
}
@end
这两个方法新增任意一个,都不会再走上文的block逻辑,而是调用willChangeValueForKey:
与didChangeValueForKey:
。
比如只新增willChangeValueForKey:
方法:
则,willChangeValueForKey:
通过MObject
的super
调用NSObject
中的方法,而didChangeValueForKey:
直接调用NSObject
的方法。
注意,这里重写方法后记得调用super
的实现,否则无法触发KVO,可以不调用super
用这种方式来阻断KVO。当然,更好的方法是使用苹果给我们提供的接口automaticallyNotifiesObserversForKey:
这里只要匹配到对应的key然后返回NO就可以了。
还有个骚操作,就是所谓的手动触发KVO,理解真正执行流程也就知道为什么在给成员变量赋值前后分别调用这两个方法可以触发KVO了。
到此为止,说的都是通过setter触发的KVO,下面来说通过KVC触发,将代码改为这样:
void kvo_test() {
MObject *obj = [MObject new];
MObserver *observer = [MObserver new];
[obj addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
// obj.name = @"test";
[obj setValue:@"test" forKey:@"name"];
[obj removeObserver:observer forKeyPath:@"name"];
}
可以看到setValue:forKey:
会调用_NSSetObjectValueAndNotify
,而后重复之前流程。
那么setValue:forKey:
是如何调用到_NSSetObjectValueAndNotify
的?
在setValue:forKey:
中可以找到这么一条指令:
进入内部后如下:
查看此时的rax
:
这里的rax
存储的就是_NSSetObjectValueAndNotify
的函数指针。
Have fun!