美文网首页
KVO底层探究

KVO底层探究

作者: lth123 | 来源:发表于2021-05-09 15:36 被阅读0次

一.什么是KVO

KVO,即 Key-Value Observing 是 Objective-C 对观察者设计模式的一种实现。

关于 KVO,即 Key-Value Observing,官方文档Key-Value Observing Programming Guide里的介绍比较简短

Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.

image.png

如果person想要知道 Account内部 balance 或者 interestRate的变化,可以使用KVO来实现监听。通常继承自NSObject的对象都可以作为被观察的对象

Person对象调用addObserver:forKeyPath:options:context:,观察Account的某个属性

image.png

Person对象需要实现observeValueForKeyPath:ofObject:change:context:方法,

image.png

最后在不需要监听时,需要移除监听者该Person实例必须通过发送消息注销removeObserver:forKeyPath:

二.KVO的底层实现

定义person类

@interface Person : NSObject
@property (assign, nonatomic) int age;
@end

在ViewController中定义两个person属性

@interface ViewController ()
@property (strong, nonatomic) Person *person1;
@property (strong, nonatomic) Person *person2;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[Person alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[Person alloc] init];
    self.person2.age = 2;

    NSLog(@"person1添加KVO监听之前 - \n%@ %@\n%@ %@",object_getClass(self.person1), [self.person1 class],object_getClass(self.person2),[self.person2 class]);

    // 给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"18"];

    NSLog(@"person1添加KVO监听之后 -\n %@ %@\n%@ %@",object_getClass(self.person1),[self.person1 class],object_getClass(self.person2),[self.person2 class]);
}

@end

打印结果如下

person1添加KVO监听之前 -
Person Person
Person Person
person1添加KVO监听之后 -
NSKVONotifying_Person Person
Person Person

从上面的打印结果可以看出

  • person1在添加KVO之后,变成了NSKVONotifying_Person类型的对象,person2,由于没有添加KVO,没有任何变化。
    -person1添加 KVO之后 object_getClass(self.person1)和[self.person1 class]的打印结果不同,说明class方法被重写了。


    01.png

从01图片中也可以看出,_person1成员变量的isa指向了NSKVONotifying_Person类对象

    NSLog(@"person1添加KVO监听之前 - \n%@ %@",[object_getClass(self.person1) superclass], [[self.person1 class] superclass]);

    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"18"];

    NSLog(@"person1添加KVO监听之后 - \n%@ %@",[object_getClass(self.person1) superclass], [[self.person1 class] superclass]);

打印结果如下

person1添加KVO监听之前 -
NSObject NSObject
person1添加KVO监听之后 -
Person NSObject

通过这次打印可以看出
NSKVONotifying_Person是继承自 Person类


// 重写person的这个三个方法
- (void)setAge:(int)age
{
    _age = age;
    
    NSLog(@"setAge:");
}


- (void)willChangeValueForKey:(NSString *)key
{
    [super willChangeValueForKey:key];
    
    NSLog(@"willChangeValueForKey");
}

- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"didChangeValueForKey - begin");
    
    [super didChangeValueForKey:key];
    
    NSLog(@"didChangeValueForKey - end");
}

// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

修改person的age属性,打印日志如下

willChangeValueForKey
setAge:
didChangeValueForKey - begin
监听到<Person: 0x600002dd0750>的age属性值改变了 - {
kind = 1;
new = 21;
old = 1;
} - 18
didChangeValueForKey - end

可以看到当修改person的age值时,会调用willChangeValueForKey,然后调用setAge: 方法,最后调用didChangeValueForKey,在didChangeValueForKey方法中调用observeValueForKeyPath,通知监听者,并把值传出去。

02.png

从图02堆栈中可以看到在 didChangeValueForKey会调用监听者的observeValueForKeyPath。

    NSLog(@"person1添加KVO监听之前 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);

    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"18"];

    NSLog(@"person1添加KVO监听之后 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);

打印结果如下:

person1添加KVO监听之前 - 0x10bdd24b0 0x10bdd24b0
person1添加KVO监听之后 - 0x10c130216 0x10bdd24b0

person1的setAge:方法已经改了,此时已经是他的子类NSKVONotifying_Person的方法,因为person1的isa指向了NSKVONotifying_Person,而NSKVONotifying_Person重写了setAge:方法,所以此时setAge:的地址变了。

03.png

如图03,当我们自己实现NSKVONotifying_Person类时,控制台打印如下:

KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class

KVO已经失效

KVO 与Runtime

KVO 的实现也依赖于 Objective-C 的 Runtime,官方文档《Key-Value Observing Programming Guide》中在《Key-Value Observing Implementation Details》部分简单提到它的实现:

Automatic key-value observing is implemented using a technique called isa-swizzling. The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data. 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. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance. You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

KVO的实现

当你观察一个对象(称该对象为被观察对象)时,一个新的类(NSKVONotifying_MJPerson)会动态被创建。这个类继承自被观察对象所对应类(MJPerson),并重写该被观察属性的 setter 方法;将对象的isa指向新生产的类(NSKVONotifying_MJPerson),这样在修改属性值的时候,就会调用到新类的setter方法,在新类的setter方法中通知监听者。

// 获取一个类的方法列表
- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    
    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[I];
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    
    // 释放
    free(methodList);
    
    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[MJPerson alloc] init];
    self.person2.age = 2;
    
    // 给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"18"];
    
    [self printMethodNamesOfClass:object_getClass(self.person1)];
    [self printMethodNamesOfClass:object_getClass(self.person2)];
}

打印如下:

NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,
MJPerson setAge:, age,

通过打印我们可以看到NSKVONotifying_MJPerson重写了 setAge:, class, dealloc, _isKVOA这几个方法。

子类重写了4个方法

  • setAge

当修改age时,通过isa找到NSKVONotifying_MJPerson中的setAge方法,在新的类中会重写对应的set方法,在setter方法中调用了 _NSSetIntValueAndNotify (),在这个方法中会按顺序调用willChangeValueForKey,[super setAge],didChangeValueForKey

  • _NSSetIntValueAndNotify ()
  • (void)willChangeValueForKey:(NSString *)key
  • [super setAge]
  • (void)didChangeValueForKey:(NSString *)key


    image.png
  • class
  • (Class)class
    {
    return [Person class];
    }
    通过上面的打印可以看到person的class返回值是Person,并不是NSKVONotifying_MJPerson。重写class方法是为了我们调用它的时候返回跟重写继承类之前同样的内容, 为了 isKindOfClass能够返回真确的结果
  • dealloc

销毁新生成的NSKVONotifying_类。

  • _isKVOA

用来标示该类是一个 KVO 机制声称的类

通过终端命令可以查看Foundation中的信息:

nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation

00000000001c1077 t ____NSSetBoolValueAndNotify_block_invoke
0000000000054564 t ____NSSetCharValueAndNotify_block_invoke
00000000000c54be t ____NSSetDoubleValueAndNotify_block_invoke
000000000010031c t ____NSSetFloatValueAndNotify_block_invoke
0000000000134c0a t ____NSSetIntValueAndNotify_block_invoke
0000000000051e8c t ____NSSetLongLongValueAndNotify_block_invoke
00000000001c10e2 t ____NSSetLongValueAndNotify_block_invoke
00000000000901aa t ____NSSetObjectValueAndNotify_block_invoke
00000000001233b0 t ____NSSetPointValueAndNotify_block_invoke
00000000001c11a3 t ____NSSetRangeValueAndNotify_block_invoke
0000000000090d64 t ____NSSetRectValueAndNotify_block_invoke
00000000001c1146 t ____NSSetShortValueAndNotify_block_invoke
00000000001209a3 t ____NSSetSizeValueAndNotify_block_invoke

三.KVO的缺陷

mikeash.com: just this guy, you know?KVO Considered Harmful这两篇文章中描述了kvo的一些缺陷。

  • 所有的 observe 处理都放在一个方法里

监听者所有的回调都在observeValueForKeyPath:ofObject:change:context:中。如果监听者多了,需要做各种判断。

  • KVO是字符串类型的

KVO 中的 keyPath 必须是NSString,使得编译器没办法在编译阶段将错误的 keyPath 给找出来;譬如很容易将「contentSize」写成「contentsize」;硬编码很容易出错。必须使用NSStringFromSelector(@selector(contentSize))

  • KVO要求您自己处理父类

如果父类也在监听时,需要在observeValueForKeyPath中调用父类的observeValueForKeyPath

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {  
    if (object == _tableView && [keyPath isEqualToString:@"contentSize"]) {  
        [self configureView];  
    } else {  
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];  
    }  
} 
  • 注销时KVO可能崩溃

多次相同的 remove observer 会导致 crash

四.如何安全的移除KVO

  • @try catch
    @try {
        [self.person1 removeObserver:self
                        forKeyPath:NSStringFromSelector(@selector(age))
                           context:nil];
    }
    @catch (NSException *exception) {
        NSLog(@"%@",exception);
    }

Cannot remove an observer <ViewController 0x7fa514c06890> for the key path "age" from <Person 0x60000005c490> because it is not registered as an observer.
当多次移除时,会cath到这个错误,不会造成crash

  • 利用runtime获取真实类型,判断是否需要移除
    Class kvoCls = object_getClass(self.person1);
    // 如果包含NSKVONotifying_,则说明对象被监听了,需要移除
    if ([NSStringFromClass(kvoCls) containsString:@"NSKVONotifying_"]) {
        NSLog(@"移除KVO");
        [self.person1 removeObserver:self forKeyPath:@"age"];
    }else{
        NSLog(@"已经移除");
    }

如果真实类型包含NSKVONotifying_字符串,说明被监听了,可以移除

五.手动触发KVO

调用下面两个方法就可以触发

    [self.person1 willChangeValueForKey:@"age"];
    [self.person1 didChangeValueForKey:@"age"];

本文参考


《Key-Value Observing Programming Guide》
https://www.mikeash.com/pyblog/key-value-observing-done-right.html
https://khanlou.com/2013/12/kvo-considered-harmful/
https://halfrost.com/how_to_use_runtime/
https://nshipster.com/key-value-observing/
KVO底层实现

相关文章

  • KVO底层探究

    一.什么是KVO KVO,即 Key-Value Observing 是 Objective-C 对观察者设计模式...

  • 2018-02-14

    探究KVO的底层实现原理 addObserver:forKeyPath:options:context:各个参数的...

  • KVO 底层实现探究

    KVO概述 键值观察Key-Value-Observer就是观察者模式。 观察者模式的定义:一个目标对象管理所有依...

  • 探究KVO底层原理

     本文将会分成三部分,一是简述KVO的底层原理,二是详解系统的KVO,三是自己手动实现KVO,我们通过断点调试、N...

  • iOS面试题整理

    1.探究KVO的底层实现原理 https://www.jianshu.com/p/829864680648 ·KV...

  • 自定义KVO

    导语: 如果对KVO原理不是很熟悉的,可以参考下简书另一篇文章《ios KVO原理探究》,主要是通过模拟KVO底层...

  • 探究KVC的底层实现原理

    慕课网地址 以前写了关于的实现原理的文章,探究KVO的底层实现原理,现在我们也探究一下的底层实现 原理 的全称是K...

  • iOS探究KVO底层并手写KVO

    我们都知道苹果的KVO可以为我们提供观察属性的方法,它可以实现监听属性的改变并得到通知。既然苹果没有给我们开源,那...

  • 底层原理探究(一)KVO

    问题:1、KVO的使用?实现原理?(为什么要创建子类来实现)2、KVC的使用?实现原理?(KVC拿到key以后,是...

  • 十:KVO底层原理探究

    KVO介绍: KVO,全称为Key-Value observing,中文名为键值观察,KVO是一种机制,它允许将其...

网友评论

      本文标题:KVO底层探究

      本文链接:https://www.haomeiwen.com/subject/jsmmdltx.html