阅读 102

十五:底层探索 - KVO

一: 什么是KVO?

KVO:全名为Key-Value Observing,通常我们将其称之为键值观察,它是OC中观察者模式的一种体现,NSNotification也是观察者模式的一种,每次当被观察对象的某个属性值发生改变时,注册的观察者便能获得通知。

观察者模式:当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。

KVO就是通过观察特定的key,来监听其值的变化,而KVO是基于KVC的。

7c88b93019d54ae19836dd4cb09cd1ad~tplv-k3u1fbpfcp-watermark.image.png 图片来源:KVO总结

二: 如何使用

2.1 基本使用

KVO使用三步走:

  1. 添加/注册KVO监听
  2. 实现监听方法以接收属性改变通知
  3. 移除KVO监听

2.1.1 添加观察者

#import <Foundation/Foundation.h>
@class WYStudent;

NS_ASSUME_NONNULL_BEGIN

@interface WYPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
@property (nonatomic, strong) NSMutableArray *dateArray;
@property (nonatomic, strong) WYStudent *student;
@end
复制代码

添加观察

[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
复制代码

1.observer:观察者,也就是KVO通知的订阅者。订阅着必须实现observeValueForKeyPath:ofObject:change:context:方法
2.keyPath:描述将要观察的属性,相对于被观察者。
3.options:KVO的一些属性配置;有四个选项。

NSKeyValueObservingOptionNew:观察新值 NSKeyValueObservingOptionOld:观察旧值 NSKeyValueObservingOptionInitial:观察初始值,如果想在注册观察者后,立即接收一次回调,可以加入该枚举值 NSKeyValueObservingOptionPrior:分别在值改变前后触发方法(即一次修改有两次触发)

4.context: 上下文,这个会传递到订阅着的函数中,用来区分消息,所以应当是不同的。(解决了性能问题和可读性问题)

同样可以观察Personstudentage属性 [self.person addObserver:self forKeyPath:@"student.age" options:NSKeyValueObservingOptionNew context:NULL];

2.1.2 实现监听

实现observeValueForKeyPath:ofObject:change:context:方法

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}
复制代码

2.1.3 移除观察者

切记要移除观察者,否则会出现一些奇怪的Bug。

[self.person removeObserver:self forKeyPath:@"nick" context:NULL];
复制代码

2.1.4 KVO的触发方式

在上面的2.1中,其实就是依赖于KVO的自动触发,以下方式都会自动触发

使用点语法
使用setter方法
使用KVC的setValue:forKey:方法
使用KVC的setValue:forKeyPath:方法

那么如何才能手动触发KVO呢?
在Person中实现以下方法,此方法默认返回YES,当返回NO的时候就关闭了自动触发

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}
复制代码

接下来我们需要手动调用两个关键方法:

  • (void)willChangeValueForKey:(NSString *)key; 值改变之前调用
  • (void)didChangeValueForKey:(NSString *)key; 值改变之后调用

在Person类set方法中调用上面的两个方法实现手动触发

- (void)setNick:(NSString *)nick{
    [self willChangeValueForKey:@"nick"];
    _nick = nick;
    [self didChangeValueForKey:@"nick"];
}
复制代码

2.2 进阶使用

2.2.1 监听集合对象

若按照常规的方式来监听集合对象的变化是无法正常监听的,KVO主动触发主要是依赖setter方法,而集合添加元素是压根没有setter方法的。

我们可以使用KVCmutableArrayValueForKey:来获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO的监听方法。

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person  = [WYPerson new];
    self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
    // 🌹添加观察
    [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];

}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // KVC 集合 array
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}

// 🌹 监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    // fastpath
    // 性能 + 代码可读性
    NSLog(@"%@",change);
}

// 🌹 移除监听
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"dateArray"];

}

复制代码

2.2.2 键值观察依赖建

有些情况下,一个属性的改变依赖于别的一个或多个属性的改变,也就是说当别的属性改了,这个属性也会跟着改变。比如Person类中的downloadProgress,依赖writtenDatatotalData的变化。

添加观察

[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
复制代码

Person类中

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

2.2.3 解决新旧值相等时候不触发的问题

关闭自动触发,由我们手动去触发即可。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    BOOL automatic = YES;
    if ([key isEqualToString:@"nick"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}

- (void)setNick:(NSString *)nick
{
    if (![_name isEqualToString:nick]) {
        [self willChangeValueForKey:@"nick"];
        _name = name;
        [self didChangeValueForKey:@"nick"];
    } 
}

复制代码

三: KVO的实现原理

苹果使用了isa混写技术(isa-swizzling)来实现KVO。当我们调用了addObserver:forKeyPath:options:context:方法,为instance被观察对象添加KVO监听后,系统会在运行时利用Runtime API动态创建instance对象所属类A的子类NSKVONotifying_A,并且让instance对象的isa指向这个全新的子类,并重写原类A的被观察属性的setter方法来达到可以通知所有观察者对象的目的。
这个子类的isa指针指向它自己的meta-class对象,而不是原类的meta-class对象。
重写的setter方法的SEL对应的IMPFoundation中的_NSSetXXXValueAndNotify函数(XXXKey的数据类型),当被观察对象的属性发送改变时,会调用_NSSetXXXValueAndNotify函数,这个函数中会调用:

  • willChangeValueForKey:方法
  • 父类原来的setter方法
  • didChangeValueForKey:方法(内部会触发监听器即观察对象observer的监听方法:observeValueForKeyPath:ofObject:change:context:

  在移除KVO监听后,被观察对象的isa会指回原类A,但是NSKVONotifying_A类并没有销毁,还保存在内存中,至于验证的话可以从A页面pushB页面,pop回到A之后打印类和子类(打印方法如下)。

- (void)printClasses:(Class)cls {
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 获取所有已注册的类
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"WYViewController:classes = %@", mArray);
}
复制代码

那么,既然生成了子类NSKVONotifying_A,它有什么方法呢?是否继承了父类的方法,哪些方法又被重写了呢?

使用如下方法进行打印

🌹 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{

    unsigned int count = 0;

    Method *methodList = class_copyMethodList(cls, &count);

    for (int i = 0; i<count; i++) {

        Method method = methodList[i];

        SEL sel = method_getName(method);

        IMP imp = class_getMethodImplementation(cls, sel);

        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);

    }

    free(methodList);

}
复制代码

打印结果:

**2021-08-19 17:57:36.410107+0800 002---KVO原理探讨[83639:3551058] 
setNickName:-0x180796b78**

**2021-08-19 17:57:36.410324+0800 002---KVO原理探讨[83639:3551058] 
class-0x18079564c**

**2021-08-19 17:57:36.410474+0800 002---KVO原理探讨[83639:3551058] 
dealloc-0x1807953d0**

**2021-08-19 17:57:36.410612+0800 002---KVO原理探讨[83639:3551058] 
_isKVOA-0x1807953c8**
复制代码

打印之后我们发现有四个方法:setNickName,classdealloc_isKVOA,那么我们如何去判断他们是继承还是重写的呢?

我们创建WYPerson类的子类WYTeacher,然后使用上面提供的方法printClassAllMethod打印WYTeacher中所有的方法,但是我们发现没有打印任何方法,其实继承的方法都在元类当中,那么setNickName,classdealloc_isKVOA必然就是重写了。

  • classclass方法中返回的是父类的class对象,目的是为了不让外界知道KVO动态生成类的存在;
  • dealloc:释放KVO使用过程中产生的东西;
  • _isKVOA:用来标志它是一个KVO的类。

具体的代码探索过程大家可以参考这个 用代码探讨 KVC/KVO 的实现原理 非常的详细。

四:FBKVOController

4.1 系统KVO的缺点

在我们使用KVO的过程中,多多少少能够感受到系统KVO的不友好:

  • 使用比较麻烦,需要三个步骤:添加/注册KVO监听、实现监听方法以接收属性改变通知、 移除KVO监听,缺一不可;

  • 需要手动移除观察者,移除观察者的时机必须合适,还不能重复移除;

  • 注册观察者的代码和事件发生处的代码上下文不同,传递上下文context是通过void *指针;

  • 需要实现-observeValueForKeyPath:ofObject:change:context:方法,比较麻烦;

  • 在复杂的业务逻辑中,准确判断被观察者相对比较麻烦,有多个被观测的对象和属性时,需要在方法中写大量的if进行判断。

4.2 FBKVOController的优点

FBKVOController是 Facebook 开源的一个基于系统KVO实现的框架。支持Objective-CSwift语言。

那么FBKVOController拥有哪些优点呢?

  • 会自动移除观察者,无需我们每次去remove

  • 函数式编程,可以一行代码实现系统KVO的三个步骤;

  • 实现KVO与事件发生处的代码上下文相同,不需要跨方法传参数;

  • 增加了blockSEL自定义操作对NSKeyValueObserving回调的处理支持;

  • 每一个keyPath会对应一个block或者SEL,不需要使用if判断keyPath

  • 可以同时对一个对象的多个属性进行监听,写法简洁;

  • 线程安全。

4.3 FBKVOController的使用

// 🌹创建实例
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;

// 🌹观察
// 使用 block
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
  clockView.date = change[NSKeyValueChangeNewKey];
}];

// 使用 SEL
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew action:@selector(wy_observerAge:)];


SEL
-(void)wy_observerAge {
  NSLog(@"我来了");
}
复制代码

4.4 FBKVOController原理

FBKVOController.png

如何优雅地使用KVO(简书) 
iOS - FBKVOController 实现原理(简书)

本文参考:
iOS - 关于 KVO 的一些总结
用代码探讨 KVC/KVO 的实现原理
iOS大解密:玄之又玄的KVO(侧重汇编角度)

文章分类
iOS
文章标签