KVO 是 key-value observing 的缩写,即 “键值监听”, 可以用于监听某个对象属性值的变化
KVO 的使用
// 定义一个简单的类
@interface AYPerson : NSObject
@property(nonatomic, assign) NSInteger age;
@end
@implementation AYPerson
@end
AYPerson *p = [[AYPerson alloc] init];
// 添加观察对象
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p addObserver:self forKeyPath:@"age" options:options context:nil];
// 实现代理方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"observeValueForKeyPath:%@ ofObject:%@ change:%@ context:%@", keyPath, object, change, context);
}
// 不需要使用的时候需要移除监听
- (void)dealloc
{
[self.p1 removeObserver:self forKeyPath:@"age"];
}
KVO 的底层实现
AYPerson *p = [[AYPerson alloc] init];
p.age = 3;
self.p1 = p;
AYPerson *p2 = [[AYPerson alloc] init];
p2.age = 3;
self.p2 = p2;
NSLog(@"before------ %@:%p, %@:%p",object_getClass(p),
object_getClass(p),
object_getClass(p2),
object_getClass(p2));
NSLog(@"meta------ %@:%p, %@:%p", object_getClas(object_getClass(p)),
object_getClass(object_getClass(p)),
object_getClass(object_getClass(p2)),
object_getClass(object_getClass(p2)));
/**
before------ AYPerson:0x1042090b8, AYPerson:0x1042090b8
meta------ AYPerson:0x104209090, AYPerson:0x104209090
*/
// 添加观察对象
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p addObserver:self forKeyPath:@"age"options:options context:nil];
NSLog(@"after------ %@:%p, %@:%p", object_getClas(p),
object_getClass(p),
object_getClass(p2),
object_getClass(p2));
NSLog(@"meta------ %@:%p, %@:%p", object_getClas(object_getClass(p)),
object_getClass(object_getClass(p)),
object_getClass(object_getClass(p2)),
object_getClass(object_getClass(p2)));
/**
after------ NSKVONotifying_AYPerson:0x283d814d0, AYPerson:0x1042090b8
meta------ NSKVONotifying_AYPerson:0x283d81560, AYPerson:0x104209090
*/
给属性添加观察者之后,源类(AYPerson)会生成一个派生类(NSKVONotifying_AYPerson)这个类是动态生成的,使用了runtime方法
/* Adding Classes */
/**
* Creates a new class and metaclass.
*/
Class objc_allocateClassPair(Class superclass,
const char * name,
size_t extraBytes);
/**
* Registers a class that was allocated using \c objc_allocateClassPair.
*/
void objc_registerClassPair(Class cls);
主动添加一个类 NSKVONotifying_AYPerson
,这个时候会添加观察者失败
[general] KVO failed to allocate class pair for name NSKVONotifying_AYPerson, automatic key-value observing will not work for this class
在监听响应方法中(observeValueForKeyPath
)打断点
bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000104e94062 MYKVO`-[ViewController observeValueForKeyPath:ofObject:change:context:](self=0x00007fc066704100, _cmd="observeValueForKeyPath:ofObject:change:context:", keyPath=@"age", object=0x0000600001dc4670, change=0x0000600000ab9600, context=0x0000000000000000) at ViewController.m:67:74
frame #1: 0x00007fff2564f735 Foundation`NSKeyValueNotifyObserver + 329
frame #2: 0x00007fff25652e4f Foundation`NSKeyValueDidChange + 499
frame #3: 0x00007fff25652752 Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] + 741
frame #4: 0x00007fff2565304b Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 68
frame #5: 0x00007fff2564dc15 Foundation`_NSSetLongLongValueAndNotify + 269
frame #6: 0x0000000104e941f4 MYKVO`-[ViewController touchesBegan:withEvent:](self=0x00007fc066704100, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x0000600002ecc780) at ViewController.m:78:13
frame #7: 0x00007fff475a57c5 UIKitCore`forwardTouchMethod + 340
frame #8: 0x00007fff475a5660 UIKitCore`-[UIResponder touchesBegan:withEvent:] + 49
frame #9: 0x00007fff475b4750 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 1867
frame #10: 0x00007fff475b6338 UIKitCore`-[UIWindow sendEvent:] + 4596
frame #11: 0x00007fff47591693 UIKitCore`-[UIApplication sendEvent:] + 356
frame #12: 0x00007fff47611e5a UIKitCore`__dispatchPreprocessedEventFromEventQueue + 6847
frame #13: 0x00007fff47614920 UIKitCore`__handleEventQueueInternal + 5980
frame #14: 0x00007fff23b0d271 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
frame #15: 0x00007fff23b0d19c CoreFoundation`__CFRunLoopDoSource0 + 76
frame #16: 0x00007fff23b0c974 CoreFoundation`__CFRunLoopDoSources0 + 180
frame #17: 0x00007fff23b0767f CoreFoundation`__CFRunLoopRun + 1263
frame #18: 0x00007fff23b06e66 CoreFoundation`CFRunLoopRunSpecific + 438
frame #19: 0x00007fff38346bb0 GraphicsServices`GSEventRunModal + 65
frame #20: 0x00007fff47578dd0 UIKitCore`UIApplicationMain + 1621
frame #21: 0x0000000104e946ad MYKVO`main(argc=1, argv=0x00007ffeead6ad68) at main.m:18:12
frame #22: 0x00007fff516ecd29 libdyld.dylib`start + 1
frame #23: 0x00007fff516ecd29 libdyld.dylib`start + 1
可以看到KVO第一个响应的方法是Foundation
_NSSetLongLongValueAndNotify
获取Foundation
的Mach-O
文件之后可以使用如下指令,在terminal
中查看Foundation中是否有类似方法
$ nm Foundation | grep ValueAndNotify
000000018307f5f8 t __NSSetBoolValueAndNotify
0000000182ffb37c t __NSSetCharValueAndNotify
000000018307f868 t __NSSetDoubleValueAndNotify
0000000183039cc4 t __NSSetFloatValueAndNotify
0000000183024f30 t __NSSetIntValueAndNotify
000000018307fd50 t __NSSetLongLongValueAndNotify
000000018307fae0 t __NSSetLongValueAndNotify
0000000182ffb534 t __NSSetObjectValueAndNotify
0000000183080230 t __NSSetPointValueAndNotify
0000000183080378 t __NSSetRangeValueAndNotify
00000001830804c0 t __NSSetRectValueAndNotify
000000018307ffc0 t __NSSetShortValueAndNotify
0000000183080624 t __NSSetSizeValueAndNotify
000000018307f730 t __NSSetUnsignedCharValueAndNotify
000000018307f9a8 t __NSSetUnsignedIntValueAndNotify
000000018307fe88 t __NSSetUnsignedLongLongValueAndNotify
000000018307fc18 t __NSSetUnsignedLongValueAndNotify
00000001830800f8 t __NSSetUnsignedShortValueAndNotify
000000018307e9a8 t __NSSetValueAndNotifyForKeyInIvar
000000018307ea1c t __NSSetValueAndNotifyForUndefinedKey
000000018308080c t ____NSSetBoolValueAndNotify_block_invoke
0000000183080860 t ____NSSetCharValueAndNotify_block_invoke
0000000183080908 t ____NSSetDoubleValueAndNotify_block_invoke
000000018308095c t ____NSSetFloatValueAndNotify_block_invoke
00000001830809b0 t ____NSSetIntValueAndNotify_block_invoke
0000000183080af8 t ____NSSetLongLongValueAndNotify_block_invoke
0000000183080a58 t ____NSSetLongValueAndNotify_block_invoke
000000018308076c t ____NSSetObjectValueAndNotify_block_invoke
0000000183080c40 t ____NSSetPointValueAndNotify_block_invoke
0000000183080c94 t ____NSSetRangeValueAndNotify_block_invoke
0000000183080ce8 t ____NSSetRectValueAndNotify_block_invoke
0000000183080b98 t ____NSSetShortValueAndNotify_block_invoke
0000000183080d40 t ____NSSetSizeValueAndNotify_block_invoke
00000001830808b4 t ____NSSetUnsignedCharValueAndNotify_block_invoke
0000000183080a04 t ____NSSetUnsignedIntValueAndNotify_block_invoke
0000000183080b48 t ____NSSetUnsignedLongLongValueAndNotify_block_invoke
0000000183080aa8 t ____NSSetUnsignedLongValueAndNotify_block_invoke
0000000183080bec t ____NSSetUnsignedShortValueAndNotify_block_invoke
看来观察不同类型的属性,会调用不同的__NSSet***AndNotify
方法.
打印并比较,添加了KVO和没有添加KVO的AYPerson方法如下
NSKVONotifying_AYPerson: setAge:, class, dealloc, _isKVOA
AYPerson: age, setAge:
生成的派生类中多了 _isKVOA
方法,并重写了 setAge:, class, dealloc
-(Class)class
{
return [super class];
}
- (void)setAge:(NSInteger)age
{
[self didChangeValueForKey:@"age"];
[super setAge: age];
[self willChangeValueForKey:@"age"];
}
- (Bool)_isKVOA
{
return YES;
}
它的实现大致上是上面的代码
KVO需要注意的点
只有调用set
方法会触发KVO
,直接改成员变量的值不会触发,
可以手动调用 willChangeValueForKey:
, didChangeValueForKey:
// 直接设置属性,不会调用KVO
self->_age += 3;
// 模拟重写后set方法,手动调用KVO
[self willChangeValueForKey:@"age"];
self->_age += 3;
[self didChangeValueForKey:@"age"];
reference: MJ的iOS底层原理课