KVO
全称 KeyValueObserving
,俗称“键值监听”,可以用于监听某个对象属性值的改变;是 Objective-C
对观察者模式(Observer Pattern)
的实现;KVO
提供一种机制,当指定的对象的属性被修改后,则对象就会接受收到通知。简单的说就是每次指定的被观察的对象的属性被修改后,KVO
就会自动通知相应的观察者了。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 - (void)viewDidLoad { [super viewDidLoad]; Person *p1 = [[Person alloc] init]; Person *p2 = [[Person alloc] init]; p1.age = 10; p2.age = 2; // self 监听 p1的 age属性 NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [p1 addObserver:self forKeyPath:@"age" options:options context:nil]; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { p1.age = 2; } - (void)dealloc { [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; }
从上述代码中可以看出,在添加监听之后,p1
的 age
属性的值在发生改变时,就会通知到监听者,执行监听者的 observeValueForKeyPath
方法。并且我们知道赋值操作都是调用 set
方法,所以我们可以在 Person
类中重写 age
的 set
方法,观察 KVO
是否是在 set
方法内部做了那些操作来通知监听者。 通过代码发现即使重写了 set
方法,p1
对象和 p2
对象调用同样的 set
方法;
通过上述代码断点可以得知,p1
对象的指针指向 NSKVONotifying_Person
,p2
对象的指针指向 Person
。 得出结论,如果一个类实现 KVO
的监听,那么他的 isa 指针指向的就不是原来对象自己,而是指向了另外的一个类对象。如果一个类没有实现 KVO
的监听,那么他的 isa 指针指向的还是对象自己。
未使用 KVO
监听对象的 isa
指针
使用 KVO
监听对象的 isa
指针
NSKVONotifyin_Person
其实是 Person
的子类,是通过 runtime
动态生成的,NSKVONotifyin_Person
的 superclass
指针是指向 Person
类对象。 上述,那么 p1
对象在调用 setage
方法的时候,会根据 p1
的 isa
指针找到 NSKVONotifyin_Person
类对象,在 NSKVONotifyin_Person
中找 setage
的方法实现。NSKVONotifyin_Person
中的 setage
方法中其实调用了 Fundation
框架中 C
语言函数 _NSsetIntValueAndNotify
,_NSsetIntValueAndNotify
内部的实现,首先调用 willChangeValueForKey
即将要改变某一个属性,之后调用父类的 setage
方法修改成员变量,最后调用 didChangeValueForKey
。在 didChangeValueForKey
中会调用监听器的监听属性的值发生了改变,最终执行 observeValueForKeyPath:ofObject:change:context
方法。
验证 KVO 的实现 验证 p1
对象和 p2
对象的 setage
方法实现的地址在添加 KVO 前后有什么变化。
对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 NSLog(@"person1添加KVO监听之前 - %@ %@", object_getClass(self.person1), object_getClass(self.person2)); NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [p1 addObserver:self forKeyPath:@"age" options:options context:nil]; NSLog(@"person1添加KVO监听之后 - %@ %@", object_getClass(self.person1), object_getClass(self.person2)); 2022-05-21 17:31:23.574690+0800 Interview01[13188:7235501] setAge: 2022-05-21 17:31:23.574865+0800 Interview01[13188:7235501] setAge: 2022-05-21 17:31:23.575033+0800 Interview01[13188:7235501] person1添加KVO监听之前 - MJPerson MJPerson 2022-05-21 17:31:23.575488+0800 Interview01[13188:7235501] person1添加KVO监听之后 - NSKVONotifying_MJPerson MJPerson
方法实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 NSLog(@"person1添加KVO监听之前 - %p %p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]); NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [p1 addObserver:self forKeyPath:@"age" options:options context:nil]; NSLog(@"person1添加KVO监听之后 - %p %p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]); 2022-05-21 17:35:16.211930+0800 Interview01[13890:7253559] setAge: 2022-05-21 17:35:16.212151+0800 Interview01[13890:7253559] setAge: 2022-05-21 17:35:16.212314+0800 Interview01[13890:7253559] person1添加KVO监听之前 - 0x108791510 0x108791510 2022-05-21 17:35:16.212811+0800 Interview01[13890:7253559] person1添加KVO监听之后 - 0x1098c4e53 0x108791510
通过 lldb 工具
1 2 3 4 5 (lldb) p (IMP)0x108791510 (IMP) $0 = 0x0000000108791510 (Interview01`-[MJPerson setAge:] at MJPerson.m:13) (lldb) p (IMP)0x1098c4e53 (IMP) $1 = 0x00000001098c4e53 (Foundation`_NSSetIntValueAndNotify) (lldb)
在添加 KVO
监听之前,p1
对象和 p2
对象的 setAge
方法实现的地址相同,而在 KVO
监听之后,p1
对象的 setAge
方法实现的地址发生了变化,p1
对象的 setAge
方法的实现由 Person
类方法中的 setAge
方法转换为了 C
语言的 Foundation
框架的 _NSsetIntValueAndNotify
函数。
验证 _NSsetIntValueAndNotify
的 isa
指针指向哪里。
1 2 3 4 5 6 7 8 NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [p1 addObserver:self forKeyPath:@"age" options:options context:nil]; NSLog(@"类对象 - %p %p", object_getClass(self.person1), // self.person1.isa object_getClass(self.person2)); // self.person2.isa 2022-05-21 17:52:32.172451+0800 Interview01[16839:7328143] 类对象 - 0x600003094090 0x1050be218
验证 _NSsetIntValueAndNotify
的 metaclass
。
1 2 3 4 5 6 7 8 NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [p1 addObserver:self forKeyPath:@"age" options:options context:nil]; NSLog(@"元类对象 - %p %p", object_getClass(object_getClass(self.person1)), // self.person1.isa.isa object_getClass(object_getClass(self.person2))); // self.person2.isa.isa 2022-05-21 17:52:32.172610+0800 Interview01[16839:7328143] 元类对象 - 0x600003094120 0x1050be240
由此得出,_NSsetIntValueAndNotify
类对象的 isa
指针指向 _NSsetIntValueAndNotify
metaclass
。
NSKVONotifyin_Person
的内部实现在 Person
类中重写 willChangeValueForKey:
和 didChangeValueForKey:
方法,模拟他们的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 - (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"); } 2022-05-21 18:13:59.766153+0800 Interview01[16839:7328143] didChangeValueForKey - begin 2022-05-21 18:13:59.766818+0800 Interview01[16839:7328143] 监听到<MJPerson: 0x6000000980a0>的age属性值改变了 - { kind = 1; new = 21; old = 1; } - 123 2022-05-21 18:13:59.767129+0800 Interview01[16839:7328143] didChangeValueForKey - end
通过控制图打印内容可以看到,证明了在 didChangeValueForKey
方法内部已经调用了 observer
的 observeValueForKeyPath:ofObject:change:context:
方法。
自动触发 KVO 的监听方法 手动调用 willChangeValueForKey:
和 didChangeValueForKey:
方法;
KVO 实现机制 使用KVO分为三个步骤:
通过 addObserver:forKeyPath:options:context:
方法注册观察者,观察者可以接收 keyPath
属性的变化事件。
在观察者中实现 observeValueForKeyPath:ofObject:change:context:
方法,当 keyPath
属性发生改变后,KVO
会回调这个方法来通知观察者。
当观察者不需要监听时,可以调用 removeObserver:forKeyPath:
方法将 KVO
移除。需要注意的是,调用 removeObserver
需要在观察者消失之前,否则会导致 Crash
。
KVO 实现原理: Apple
的文档有简单提到过 KVO
的实现:
Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class …
KVO
是通过 isa-swizzling
技术实现的 ,当你观察一个对象时,一个新的类会动态被创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter
方法。重写的 setter
方法会在调用原 setter
方法之前和之后,通知所有观察对象值的更改。最后把这个对象的 isa
指针指向这个新创建的类,对象变成了新创建的类的实例,而不是原来真正的类。
KVO 缺陷 使用 KVO
时,既需要进行注册成为某个对象属性的观察者,还要在合适的时间点将自己移除,在观察者中实现 observeValueForKeyPath:ofObject:change:context:
方法,并在方法里判断这次是不是自己要观测的属性发生了变化,并且很重要的一点就是 KVO
如果使用不当很容易导致 Crash
。 例如,重复 addObserver
和 removeObserver
导致的 Crash
,Observer
被释放导致的崩溃,keyPath
传错导致的崩溃等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 @interface TestKVOViewController () @property (nonatomic, strong) Message *message; @end @implementation TestKVOViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; Message *message = [[Message alloc] init]; [message addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil]; message.text = @"hello object-c"; self.message = message; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { if ([keyPath isEqualToString:@"text"]) { NSLog(@"%@", change); } } - (void)dealloc { [self.message removeObserver:self forKeyPath:@"text"]; [self.message removeObserver:self forKeyPath:@"text"]; } 2017-06-21 12:29:03.397434+0800 ImplementKVO[18119:266230] [general] Caught exception during autorelease pool drain NSRangeException: Cannot remove an observer <TestKVOViewController 0x7faf7b502610> for the key path "text" from <Message 0x60400001eef0> because it is not registered as an observer. userInfo: (null) 2017-06-21 12:29:03.402598+0800 ImplementKVO[18119:266230] *** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <TestKVOViewController 0x7faf7b502610> for the key path "text" from <Message 0x60400001eef0> because it is not registered as an observer.' *** First throw call stack: ( 0 CoreFoundation 0x000000010efa11e6 __exceptionPreprocess + 294 1 libobjc.A.dylib 0x000000010e636031 objc_exception_throw + 48 2 CoreFoundation 0x000000010f016975 +[NSException raise:format:] + 197 3 Foundation 0x000000010e061e43 -[NSObject(NSKeyValueObserverRegistration) _removeObserver:forProperty:] + 488 4 Foundation 0x000000010e061c03 -[NSObject(NSKeyValueObserverRegistration) removeObserver:forKeyPath:] + 84 5 ImplementKVO 0x000000010dd2de9c -[TestKVOViewController dealloc] + 172 6 UIKit 0x000000010f406f74 __destroy_helper_block_.150 + 80 7 libsystem_blocks.dylib 0x0000000112a7098a _Block_release + 111 8 UIKit 0x000000010f4e01e5 -[UIViewAnimationBlockDelegate .cxx_destruct] + 58 9 libobjc.A.dylib 0x000000010e634920 _ZL27object_cxxDestructFromClassP11objc_objectP10objc_class + 127 10 libobjc.A.dylib 0x000000010e640502 objc_destructInstance + 124 11 libobjc.A.dylib 0x000000010e640539 object_dispose + 22 12 libobjc.A.dylib 0x000000010e64aa6e _ZN11objc_object17sidetable_releaseEb + 202 13 CoreFoundation 0x000000010ef0464d -[__NSDictionaryI dealloc] + 125 14 libobjc.A.dylib 0x000000010e64aa6e _ZN11objc_object17sidetable_releaseEb + 202 15 libobjc.A.dylib 0x000000010e64b1b2 _ZN12_GLOBAL__N_119AutoreleasePoolPage3popEPv + 860 16 CoreFoundation 0x000000010eeeb136 _CFAutoreleasePoolPop + 22 17 CoreFoundation 0x000000010ef27eae __CFRunLoopRun + 2350 18 CoreFoundation 0x000000010ef2730b CFRunLoopRunSpecific + 635 19 GraphicsServices 0x0000000114119a73 GSEventRunModal + 62 20 UIKit 0x000000010f41e057 UIApplicationMain + 159 21 ImplementKVO 0x000000010dd2dfef main + 111 22 libdyld.dylib 0x00000001129fe955 start + 1 23 ??? 0x0000000000000001 0x0 + 1 ) libc++abi.dylib: terminating with uncaught exception of type NSException
手动实现 KVO 1 2 3 4 5 6 7 8 9 10 11 typedef void(^ObservingBlock)(id observedObject, NSString *observedKey, id oldValue, id newValue); @interface NSObject (KVO) - (void)addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(ObservingBlock)block; - (void)removeObserver:(NSObject *)observer forKey:(NSString *)key; @end
接下来,实现 addObserver:forKey:withBlock:
方法:
检查对象的类有没有相应的 setter
方法。如果没有抛出异常;
检查对象 isa
指向的类是不是一个 KVO
类。如果不是,新建一个继承原来类的子类,并把 isa
指向这个新建的子类;
检查对象的 KVO
类重写过没有这个 setter
方法。如果没有,添加重写的 setter
方法;
添加这个观察者;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 /** 1. 通过Method判断是否有这个key对应的selector,如果没有则Crash。 2. 判断当前类是否是KVO子类,如果不是则创建,并设置其isa指针。 3. 如果没有实现,则添加Key对应的setter方法。 4. 将调用对象添加到数组中。 */ - (void)addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(ObservingBlock)block { SEL setterSelector = NSSelectorFromString(setterForGetter(key)); Method setterMethod = class_getInstanceMethod([self class], setterSelector); if (!setterMethod) { NSString *reason = [NSString stringWithFormat:@"Object %@ does not have a setter for key %@", self, key]; @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil]; return; } Class clazz = object_getClass(self); NSString *clazzName = NSStringFromClass(clazz); // if not an KVO class yet if (![clazzName hasPrefix:kKVOClassPrefix]) { clazz = [self makeKvoClassWithOriginalClassName:clazzName]; object_setClass(self, clazz); } // add our kvo setter if this class (not superclasses) doesn't implement the setter? if (![self hasSelector:setterSelector]) { const char *types = method_getTypeEncoding(setterMethod); class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types); } ObservationInfo *info = [[ObservationInfo alloc] initWithObserver:observer Key:key block:block]; NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kKVOAssociatedObservers)); if (!observers) { observers = [NSMutableArray array]; objc_setAssociatedObject(self, (__bridge const void *)(kKVOAssociatedObservers), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } [observers addObject:info]; }
第一步,先通过 setterForGetter()
方法获得相应的 setter
的名字 (SEL)
。也就是把 key
的首字母大写,然后前面加上 set
后面加上 :
,这样 key
就变成了 setKey:
。然后再用 class_getInstanceMethod
去获得 setKey:
的实现 (Method)
。如果没有,自然要抛出异常。
第二步,我们先看类名有没有我们定义的前缀。如果没有,我们就去创建新的子类,并通过 object_setClass()
修改 isa
指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 /** 1. 判断是否存在KVO类,如果存在则返回。 2. 如果不存在,则创建KVO类。 3. 重写KVO类的class方法,指向自定义的IMP。 */ - (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName { NSString *kvoClazzName = [kKVOClassPrefix stringByAppendingString:originalClazzName]; Class clazz = NSClassFromString(kvoClazzName); if (clazz) { return clazz; } // class doesn't exist yet, make it Class originalClazz = object_getClass(self); Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0); // grab class method's signature so we can borrow it Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class)); const char *types = method_getTypeEncoding(clazzMethod); class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types); objc_registerClassPair(kvoClazz); return kvoClazz; }
当动态创建新的类需要调用 objc/runtime.h
中定义的 objc_allocateClassPair()
函数。传一个父类,类名,然后额外的空间(通常为 0),它返回给你一个类。然后就给这个类添加方法,也可以添加变量。这里,我们只重写了 class
方法。跟 Apple
一样,这时候我们也企图隐藏这个子类的存在。最后 objc_registerClassPair()
告诉 Runtime
这个类的存在。
第三步,重写 setter
方法。新的 setter
在调用原 setter
方法后,通知每个观察者(调用之前传入的 block ):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 static void kvo_setter(id self, SEL _cmd, id newValue) { NSString *setterName = NSStringFromSelector(_cmd); NSString *getterName = getterForSetter(setterName); if (!getterName) { NSString *reason = [NSString stringWithFormat:@"Object %@ does not have setter %@", self, setterName]; @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil]; return; } id oldValue = [self valueForKey:getterName]; struct objc_super superclazz = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)) }; // cast our pointer so the compiler won't complain void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper; // call super's setter, which is original class's setter method objc_msgSendSuperCasted(&superclazz, _cmd, newValue); // look up observers and call the blocks NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kKVOAssociatedObservers)); for (ObservationInfo *each in observers) { if ([each.key isEqualToString:getterName]) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ each.block(self, getterName, oldValue, newValue); }); } } }
在这里会对 objc_msgSendSuper
进行类型转换。因为在 Xcode 6
之后, LLVM
会对 objc_msgSendSuper
以及 objc_msgSend
做严格的类型检查,如果不做类型转换。Xcode
会提示有 too many arguments
的错误。(在 WWDC 2014 的视频 What new in LLVM 中有提到过这个问题。)
最后一步,把这个观察的相关信息存在 associatedObject
里。观察的相关信息(观察者,被观察的 key
, 和传入的 block
)封装在 ObservationInfo
类里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @interface ObservationInfo : NSObject @property (nonatomic, weak) NSObject *observer; @property (nonatomic, copy) NSString *key; @property (nonatomic, copy) ObservingBlock block; @end @implementation ObservationInfo - (instancetype)initWithObserver:(NSObject *)observer Key:(NSString *)key block:(ObservingBlock)block { self = [super init]; if (self) { _observer = observer; _key = key; _block = block; } return self; } @end
KVO crash 防护方案