iOS Runtime之KVO

Runtime系列导读

  • iOS Runtime之方法查找
  • iOS Runtime之方法替换
  • iOS Runtime之KVO
  • iOS Runtime之KVC
  • iOS 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 *)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 *)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 for the key path "age" from because it is not registered as an observer.'**

你可能感兴趣的:(iOS Runtime之KVO)