KVO简述

706 阅读3分钟

下班随拍-月色

概念

KVO 是 Objective-C 对 观察者模式(Observer Pattern)的实现。当被观察对象的某个属性发生更改时,观察者对象会获得通知。一般继承自NSObject的对象都默认支持KVO。KVO是响应式编程的代表。

基本使用

  1. 注册观察者,实施监听
    _p = [[Person alloc] init];
    /*
        1. p: 被观察对象
        2. observer: 观察者
        3. keyPath: 被观察属性
        4. options: 属性配置
        5. context: 上下文, 用来区分不同的KVO监听, 注册观察者时context传过来的值(一般传nil就行)
     */
//    options4个枚举值
//    NSKeyValueObservingOptionNew: // change字典包括改变后的值
//    NSKeyValueObservingOptionOld: // change字典包括改变前的值
//    NSKeyValueObservingOptionInitial: // 注册后立刻触发KVO通知
//    NSKeyValueObservingOptionPrior: // 值改变前是否也要通知 (这个key决定了是否在改变前改变后通知两次)

    //监听属性的set方法有没有调用
    [_p addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
  1. 实现观察方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
}

3.移除监听

[_p removeObserver:self forKeyPath:@"age"];

手动触发

KVO一般是自动触发,被观察的值改变了就会调用观察方法,但是有时候也会在某种特定的情况下才调用观察方法,这时候就可以用手动模式来实现。

  1. 在被观察类中重写+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    // 修改为手动模式
    return NO;
}
  1. 手动触发
[self.person willChangeValueForKey:@"age"];
[self.person didChangeValueForKey:@"age"];

底层实现原理

KVO的底层实现大概是下面几个步骤:

1.runtime动态生成Person类的子类(也叫派生类)
2. 修改监听对象的isa指针指向派生类。
3. 重写NSKVONotifying_Person的属性set方法,目的:监听属性有没有变化
4.在set方法调用时调用observer的- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context方法,告诉监听者监听属性发生了变化

通过断点我们可以看到isa指向的类确实发生了变化 注册观察者之前.png

注册观察者之后.16.png

我们也可以尝试自己简单实现一下KVO的实现

  1. 添加一个NSObject 的分类NSObject+KVO.h、跟一个 Person 的分类 NSKVONotifying_Person.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (KVO)

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

@end

NS_ASSUME_NONNULL_END


#import "NSObject+KVO.h"
#import <objc/message.h>


@implementation NSObject (KVO)

- (void)lcx_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
    
    //修改isa指针
    object_setClass(self, NSClassFromString(@"NSKVONotifying_Person"));
    
    //保存观察者对象
    //self -> p;
    //动态添加属性
    //这里选择OBJC_ASSOCIATION_RETAIN,因为要防止observer对象,这也是系统KVO必须要移除观察者的原因,就是为了引用计数减1
    objc_setAssociatedObject(self, @"observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

----------------------------------------------------------------


#import "Person.h"

NS_ASSUME_NONNULL_BEGIN

@interface NSKVONotifying_Person : Person

@end

NS_ASSUME_NONNULL_END


#import "NSKVONotifying_Person.h"
#import <objc/message.h>

@implementation NSKVONotifying_Person

- (void)setAge:(NSInteger)age {
    //super: 是一个标志,去执行父类方法
    [super setAge:age];
    
    //调用观察者的observeValueForKeyPath方法
    //动态获取属性
    id observer = objc_getAssociatedObject(self, @"observer");
    //对observer发送observeValueForKeyPath方法,告诉监听者属性值的变化
    [observer observeValueForKeyPath:@"age" ofObject:self change:nil context:nil];
}

@end

  1. NSObject+KVO.h类里面添加- (void)lcx_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context方法,并在该方法里面修改isa指针指向NSKVONotifying_Person类并保存observer

  2. NSKVONotifying_Person.h类里面重写set方法,并在set方法里面获取observer,并对observer发送observeValueForKeyPath方法,告诉监听者属性值的变化。

不过苹果内部的的实现肯定要复杂的多,这里就就是简单实现下调用流程,其实通知的实现跟KVO也类似,只是通知是一对多的关系。这里大家也可以尝试做一些别的操作例如注册监听方法的时候传进来一个block属性,在set方法里面调用block,实现一个block版的KVO,使代码更加内聚。

持续更新改正中......