日志6. KVO

223 阅读5分钟

键值观察

1、KVO实现原理

1、当实例对象 进行KVO观察时候,会利用RuntimeAPI动态生成一个子类,然后将对象的isa指向新生成的子类

 2、KVO本质上是监听属性的setter方法,只要被观察对象有成员变量和对应的set方法,就会调用Foundation的_NSSetValueAndNotify函数
这个函数内部会执行 willChangeVlaueForKey函数、
父类的setter方法
和didChangeVlaueForKey的方法(

didChangeVlaueForKey 方法内部会触发监听器的observeValueForKeyPath: ofObject: context:函数

 3、子类会重写父类的set、class、dealloc、_isKVOA方法

    • 重写的class方法可以指回TCJPerson  原

 4、当观察对象移除所有的监听后,会将观察对象的isa指向原来的类

 5、当观察对象的监听全部移除后,动态生成的类不会注销,而是留在下次观察时候再使用,避免反复创建中间子类

KVO(Key-Value Observing)是苹果提供的一套事件通知机制,这种机制允许将其他对象的特定属性的更改通知给对象.
iOS开发者可以使用KVO来检测对象属性的变化、快速做出响应,这能够为我们在开发强交互、响应式应用以及实现视图和模型的双向绑定时提供大量的帮助。
要理解KVO,必须先理解KVC,因为键值观察是建立在键值编码的基础上

KVONSNotificatioCenter有什么区别呢?

  • 相同点
    • 1、两者的实现原理都是观察者模式,都是用于监听

    • 2、都能实现一对多的操作

      KVO观察中的一对多,意思是通过注册一个KVO观察者,可以监听多个属性的变化

  • 不同点
    • 1、KVO只能用于监听对象属性的变化,并且属性名都是通过NSString来查找,编译器不会帮你检测对错和补全,纯手敲会比较容易出错

    • 2、NSNotification的发送监听(post)的操作我们可以控制,KVO由系统控制

    • 3、KVO可以记录新旧值变化

KVO使用三部曲:

  • 注册观察者

    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
    复制代码
    
  • 实现回调

    - (void)observeValueForKeyPath:(NSString *)keyPath 
       ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change 
       context:(void *)context {
        if ([keyPath isEqualToString:@"name"]) NSLog(@"%@", change);
    }
    
    复制代码
    
  • 移除观察者

    [self.person removeObserver:self forKeyPath:@"name"];
    

通俗的讲,context上下文主要是用于区分不同对象的同名属性,从而在KVO回调方法中可以直接使用context进行区分,可以大大提升性能,以及代码的可读性

//定义context
static void *PersonNameContext = &PersonNameContext;
static void *StudentNameContext = &StudentNameContext; 

苹果官方推荐的方式是 —— 在init的时候进行addObserver,在deallocremoveObserver,这样可以保证addremove是成对出现的,这是一种比较理想的使用方式

比如有一个下载任务的需求,根据总下载量totalData和当前已下载量writtenData来得到当前下载进度downloadProgress,这个需求就有两种实现方式:

  • 分别观察总下载量totalData和当前已下载量writtenData两个属性,其中一个属性发生变化时计算求值当前下载进度downloadProgress

  • 实现keyPathsForValuesAffectingValueForKey方法,并观察downloadProgress属性

    • (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{

      NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqualToString:@"downloadProgress"]) { NSArray *affectingKeys = @[@"totalData", @"writtenData"]; keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; } return keyPaths; } 复制代码

但仅仅是这样还不够——这样只能监听到回调,但还没有完成downloadProgress赋值——需要重写getter方法

KVO观察 可变数组

如题:TCJPerson下有一个可变数组dataArray,现观察之,问点击屏幕是否打印?

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [TCJPerson new];
    [self.person addObserver:self forKeyPath:@"dataArray" options:(NSKeyValueObservingOptionNew) context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"dataArray"]) NSLog(@"%@", change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.person.dataArray addObject:@"1"];
}

复制代码

答:不会 分析:

  • KVO是建立在KVC的基础上的,而可变数组直接添加是不会调用Setter方法

  • 可变数组dataArray没有初始化,直接添加会报错

    // 初始化可变数组 self.person.dataArray = @[].mutableCopy; // 调用setter方法 [[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"2"];

.动态子类探索

➊首先得明白动态子类观察的是什么?下面观察属性变量nickName和成员变量name来找区别

两个变量同时发生变化,但只有属性变量监听到回调——说明动态子类观察的是setter方法

1.automaticallyNotifiesObserversForKeyYES时注册观察属性会生成动态子类NSKVONotifying_XXX
2.动态子类观察的是setter方法
3.动态子类重写了观察属性的setter方法、deallocclass_isKVOA方法 - setter方法用于观察键值 - dealloc方法用于释放时对isa指向进行操作 - class方法用于指回动态子类的父类 - _isKVOA用来标识是否是在观察者状态的一个标志位
4.dealloc之后isa指向元类
5.dealloc之后动态子类不会销毁

四、自定义KVO

新建一个NSObject+TCJKVO的分类,开放注册观察者方法

-(void)cj_addObserver:(NSObject *)observer 
         forKeyPath:(NSString *)keyPath 
               block:(TCJKVOBlock)block;

1.判断当前观察值keypath是否存在/setter方法是否存在

一开始想的是判断属性是否存在,虽然父类的属性不会对子类造成影响,但是分类中的属性虽然没有setter方法,但是会添加到propertiList中去——最终改为去判断setter方法

2. 判断观察属性的automaticallyNotifiesObserversForKey方法返回的布尔值

3.动态生成子类,添加class方法指向原先的类

4.isa重指向——使对象的isa的值指向动态子类

5.保存信息 由于可能会观察多个属性值,所以以属性值-模型的形式一一保存在数组中

②.添加setter方法并回调

往动态子类添加setter方法

③.销毁观察者

往动态子类添加dealloc方法