美文网首页
iOS Runtime之KVO

iOS Runtime之KVO

作者: 谢二九 | 来源:发表于2022-06-26 08:58 被阅读0次

    Runtime系列导读

    KVO简介

    全称Key-Value Observing,KVO是Object-C中定义的一个通知机制,其定义了一种对象间监控对方状态的改变,并做出反应的机制。对象可以为自己的属性注册观察者,当这个属性的值发生了改变,系统会对这些注册的观察者做出通知。

    KVO用法

    添加监听

    - (void)addObserver:(NSObject *)observer  
             forKeyPath:(NSString *)keyPath  
                options:(NSKeyValueObservingOptions)options  
                context:(void *)context  
    
    • observer: 观察者对象. 其必须实现方法observeValueForKeyPath:ofObject:change:context:
    • keyPath: 被观察的属性,不能为nil
    • options: 设定通知观察者时传递的属性值的类型,具体设置可查看枚举 NSKeyValueObservingOptions
    • context: 一些其他的需要传递给观察者的上下文信息,通常设置为nil

    监听实现

    -(void)observeValueForKeyPath:(NSString *)keyPath
                         ofObject:(id)object
                           change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                          context:(void *)context
    
    • keyPath: 被观察的属性
    • object: 被观察的对象
    • change: 根据options设置,可能出现old|new,或者都有
    • context: 监听时传入的上下文信息

    KVO实现过程

    KVO的实现过程实际上是利用了OC的runtime机制,当一个实例对象添加观察者时,底层根据该实例对象所属的类动态添加了一个类(动态添加的类名就是在原来类的类名前加上NSKVONotifying_前缀),这个类是继承自原来的类的。这里以继承自NSObject的KVOTest类来举例。

    • KVOTest实现:
    @interface KVOTest : NSObject
    
    @property (nonatomic, assign) NSInteger age;
    
    
    @end
    
    @implementation KVOTest
    
    -(void)didChangeValueForKey:(NSString *)key
    {
        [super didChangeValueForKey:key];
        NSLog(@"didChangeValueForKey:%@,%p", key, self);
    }
    
    @end
    
    • 调用代码:
    -(void)testKVO2
    {
        self.test = [KVOTest new];
        self.test.age = 10;
        NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.test addObserver:self forKeyPath:@"age" options:option context:nil];
        self.test.age = 10;
    }
    
    • 监听代码:
    -(void)observeValueForKeyPath:(NSString *)keyPath
                         ofObject:(id)object
                           change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                          context:(void *)context
    {
        NSLog(@"%@ - %@" , keyPath, change);
    }
    
    • 打印日志:
    2022-06-25 21:58:32.716895+0800 StudyApp[31571:1344036] age - {
        kind = 1;
        new = 10;
        old = 10;
    }
    2022-06-25 21:58:32.716960+0800 StudyApp[31571:1344036] didChangeValueForKey:age,0x600003f14590
    

    上面实例的底层实现过程如下:

    • self.test添加观察者时,底层就利用runtime动态生成一个叫NSKVONotifying_KVOTest的类,这个类继承自KVOTest类,并重写了以下实例方法:
      • 重写class方法,不重写的话调用这个方法返回的是NSKVONotifying_KVOTest这个类,重写后返回的是原本的KVOTest类。苹果这么做的目的是为了隐藏KVO的实现细节。
      • 重写dealloc方法,在这个方法里面做一些收尾的工作。
      • 重写_isKVOA方法,这是一个私有方法,我们不必关心。
      • 重写被监听属性的setter方法,上面案例只监听了name属性,所以只需重写setName:方法。重写setter是实现KVO的关键,在setter方法里面实际是调用的Foundation框架下的_NSSetValueAndNotify方法(表示不是一个固定的,这个和监听的属性的类型有关,比如是属性是int类型的话这里就是__NSSetIntValueAndNotify,所包含的类型会在后面列出来)。
    • 然后将self.test这个实例对象的isa改为指向NSKVONotifying_KVOTest(原本是指向KVOTest类的)。
    • 当我们设置被监听属性的值时self.test.age = 10,是调用的setAge:方法,前面说了setAge:方法被重写了,所以实际上调用的是_NSSetIntValueAndNotify这个方法。这个方法实现苹果是没有开源的,无法得知其具体实现,不过可以猜出其实现流程大致如下:
      • 首先调用[self willChangeValueForKey:@"age"];这个方法。
      • 然后调用原先的setter方法的实现(比如_age = age;);
      • 再调用[self didChangeValueForKey:@"age"];这个方法。
      • 最后在didChangeValueForKey:这个方法中调用观察者的observeValueForKeyPath: ofObject: change: context:方法来通知观察者属性值发生了变化。

    KVO答疑

    如何手动触发KVO?

    • 手动调用willChangeValueForKey: 和 didChangeValueForKey:

    直接修改成员变量会触发KVO吗?

    • 不会触发KVO

    对同一个属性N次注册,修改一次该属性,observeValueForKeyPath会调用几次

    N次。

    对同一个属性一次注册,多次removeObserver,会发生什么

    crash,提示**'Cannot remove an observer <ViewController 0x7fa3fd708c60> for the key path "age" from <KVOTest 0x6000018e8790> because it is not registered as an observer.'**

    相关文章

      网友评论

          本文标题:iOS Runtime之KVO

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