KVO

30 Oct 2019

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 获取FoundationMach-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底层原理课