iOS-KVO

949 阅读5分钟

介绍

KVO:key-value-observing,键值监听机制,由NSKeyValueObserving协议提供支持,NSObject类继承了该协议,所以NSObject的子类都可使用该方法

当某个对象的属性值发生改变时,可以通过KVO监听

使用方法

  1. 注册观察者

给对象绑定一个监听器(观察者)

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

//addObserver:观察者,一般为self,例如控制器本身
//forKeyPath:要监听的属性
//options:选项(你想要拿到的属性值是新的还是旧的)
//context:一般设为nil
//options参数选择
NSKeyValueObservingOptionNew = 0x01
NSKeyValueObservingOptionOld = 0x02
  1. 实现回调方法

当监听的属性值发生改变时,监听器就会回调自身的监听方法,如下

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

//keyPath:要改变的属性
//object:要改变的属性所属的对象
//change:改变的内容
//context:上下文
  1. 触发回调方法
  2. 移除观察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
[person removeObserver:self forKeyPath:@"name"];

完整示例

//  Person.h
@property(nonatomic,copy) NSString *name;

//  ViewController.m
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self kvoTest];
}

-(void)kvoTest{
    Person *person = [[Person alloc]init];
  //添加观察者
    //    [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld context:nil];
  //修改属性值
    person.name = @"ZS";
  //修改属性值
    person.name = @"GCK";
  //移除观察者
    [person removeObserver:self forKeyPath:@"name"];
}

//实现回调方法
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@-----%@------%@",keyPath,object,change);
}
//打印结果,当选项是打印新值时
2021-03-30 16:26:26.876826+0800 KVO-test[10021:436508] name-----<Person: 0x60000039ccf0>------{
    kind = 1;
    new = ZS;
}
2021-03-30 16:26:26.877043+0800 KVO-test[10021:436508] name-----<Person: 0x60000039ccf0>------{
    kind = 1;
    new = GCK;
}
//打印结果,当选项是打印旧值时
2021-03-30 16:30:04.230746+0800 KVO-test[10092:440337] name-----<Person: 0x6000036ac700>------{
    kind = 1;
    old = "<null>";
}
2021-03-30 16:30:04.231015+0800 KVO-test[10092:440337] name-----<Person: 0x6000036ac700>------{
    kind = 1;
    old = ZS;
}

也可以通过这种方式打印新旧值

[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
//打印结果
2021-03-30 16:32:43.094120+0800 KVO-test[10142:442921] name-----<Person: 0x600003d8c530>------{
    kind = 1;
    new = ZS;
    old = "<null>";
}
2021-03-30 16:32:43.094395+0800 KVO-test[10142:442921] name-----<Person: 0x600003d8c530>------{
    kind = 1;
    new = GCK;
    old = ZS;
}

总的来说就是当你监听的属性(name)发生改变时,就会通知监听者(self,控制器),执行监听者的observeValueForKeyPath方法

应用场景

  • 当数据模型的数据发生改变时,视图组件能动态的更新

  • 监听scrollView的contentOffset属性,来完成用户滚动时动态改变某控件的属性实现效果,例如下拉刷新等

  • ......

实现原理

问题

  1. iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
  2. 如何手动触发KVO?

KVO是基于Runtime机制实现的,当某个类的属性对象第一次被观察时,系统就会在运行期间动态地创建该类的一个派生类,这个派生类中重写基类中任何被观察属性的setter方法,派生类在被重写的setter方法内实现真正的通知机制

当一个类对象第一次被观察时,系统会将isa指针指向动态生成的派生类,从而在给被监听属性赋值时执行的是派生类的setter方法

举个例子:

通过断点,可以看到在addObserver方法前,person属于Person类,执行完addObserver方法后,person属于NSKVONotifying_Person类了,也就是说一旦对象添加了KVO监听后,其isa指针会发生变化,因此set方法的执行效果也不一样了

image.png NSKVONotifying_Person其实是Person的子类,NSKVONotifying_Person是Runtime在运行时生成的

person对象在调用setName方法的时候,肯定会根据personisa指针找到NSKVONotifying_Person,在NSKVONotifying_Person中找setName的方法及实现

NSKVONotifying_PersonsetName实现原理

  • 首先调用了willChangeValueForKey:
  • 然后调用父类的setName方法对成员变量赋值
  • 最后调用 didChangeValueForKey:,这个方法会调用观察者的监听方法,最后来到观察者的observeValueForKeyPath方法

PS:KVO的这套实现机制苹果还偷偷重写了class方法,让我们误以为使用的还是当前类,从而达到了隐藏生成的派生类

回答问题:

问题

  1. iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

答:当一个对象使用了KVO监听,系统会修改这个对象的 isa指针,改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的set方法实现

  • set方法内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。

  1. 如何手动触发KVO?

答:被监听的属性值被修改时,会自动触发KVO;如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey方法和didChangeValueForKey方法,这样就可以在不改变属性值的情况下手动触发KVO,两个方法缺一不可



参考文章

iOS-KVO(键值监听)

iOS底层原理总结 - 探寻KVO本质