iOS 设计模式(五)-KVO 详解

一、KVO 简介

KVO 的全称是 Key-Value Observing,俗称“键值观察/监听”,是苹果提供的一套事件通知机制,允许一个对象观察/监听另一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发 KVO 的监听方法来通知观察者。KVO 是在 MVC 应用程序中的各层之间进行通信的一种特别有用的技术。
KVO 和 NSNotification 都是 iOS 中观察者模式的一种实现。
KVO 可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过 KVC 的 mutableArrayValueForKey: 等可变代理方法获得集合代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发 KVO 的监听方法。集合对象包含 NSArrayNSSet

二、使用

先创建一个类,作为要监听的对象。

#import 

NS_ASSUME_NONNULL_BEGIN

@interface DJModel : NSObject
@property (nonatomic, strong) NSString *name;
@end

NS_ASSUME_NONNULL_END

#import "DJModel.h"

@implementation DJModel

@end

监听实现

#import "ViewController.h"
#import "DJModel.h"
@interface ViewController ()
@property(nonatomic,strong)DJModel *model;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior;
    [self.model addObserver:self forKeyPath:@"name" options:options context:@"context"];
    self.model.name = @"123";
}
-(DJModel *)model{
    if (!_model) {
        _model = [[DJModel alloc]init];
    }
    return _model;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"keyPath:%@",keyPath);
    NSLog(@"object:%@",object);
    NSLog(@"change:%@",change);
    NSLog(@"context:%@",context);
}

- (void)dealloc {
    [self.model removeObserver:self forKeyPath:@"name"];
}

@end
1.实际应用

KVO 主要用来做键值观察操作,想要一个值发生改变后通知另一个对象,则用 KVO 实现最为合适。斯坦福大学的 iOS 教程中有一个很经典的案例,通过 KVO 在 Model 和 Controller 之间进行通信。如图所示:


2.触发监听方法的方式

KVO 触发分为自动触发和手动触发两种方式。

(1)自动触发

如果是监听对象特定属性值的改变,通过以下方式改变属性值会触发 KVO:

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

如果是监听集合对象的改变,需要通过 KVC 的 mutableArrayValueForKey: 等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发 KVO。集合对象包含 NSArrayNSSet

(2)手动触发

普通对象属性或是成员变量使用:

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

NSArray 对象使用:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

NSSet 对象使用:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

三、进阶使用

1.observationInfo 属性

observationInfo 属性是 NSKeyValueObserving.h 文件中系统通过分类给 NSObject 添加的属性,所以所有继承于 NSObject 的对象都含有该属性;
可以通过 observationInfo 属性查看被观察对象的全部观察信息,包括 observerkeyPathoptionscontext 等。

@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
2.context 的使用

注册方法 addObserver:forKeyPath:options:context: 中的 context 可以传入任意数据,并且可以在监听方法中接收到这个数据。

context 作用:标签-区分,可以更精确的确定被观察对象属性,用于继承、 多监听;也可以用来传值。
KVO 只有一个监听回调方法 observeValueForKeyPath:ofObject:change:context:,我们通常情况下可以在注册方法中指定 contextNULL,并在监听方法中通过 objectkeyPath 来判断触发 KVO 的来源。

但是如果存在继承的情况,比如现在有 Person 类和它的两个子类 Teacher 类和 Student 类,person、teacher 和 student 实例对象都对象的 name 属性进行观察。问题:
当 name 发生改变时,应该由谁来处理呢?
如果都由 person 来处理,那么在 Person 类的监听方法中又该怎么判断是自己的事务还是子类对象的事务呢?
这时候通过使用 context 就可以很好地解决这个问题,在注册方法中为 context 设置一个独一无二的值,然后在监听方法中对 context 值进行检验即可。

苹果的推荐用法:用 context 来精确的确定被观察对象属性,使用唯一命名的静态变量的地址作为 context 的值。可以为整个类设置一个 context,然后在监听方法中通过 objectkeyPath 来确定被观察属性,这样存在继承的情况就可以通过 context 来判断;也可以为每个被观察对象属性设置不同的 context,这样使用 context 就可以精确的确定被观察对象属性。

context 优点:嵌套少、性能高、更安全、扩展性强。
context 注意点:

  • 如果传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问 context 就可能导致 Crash;
  • 空传 NULL 而不应该传 nil
3.监听集合对象

KVO 可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过 KVC 的 mutableArrayValueForKey: 等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发 KVO 的监听方法。集合对象包含 NSArrayNSSet。(注意:如果直接对集合对象进行操作改变,不会触发 KVO。)

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [HTPerson new];
    self.person.mArray = [NSMutableArray arrayWithCapacity:5];
    [self.person addObserver:self forKeyPath:@"mArray" options:(NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld) context:NULL];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//    [self.person.mArray addObject:@"2"]; //如果直接对数组进行操作,不会触发KVO
    NSMutableArray *array = [self.person mutableArrayValueForKey:@"mArray"];
    [array addObject:@"1"];
    [array replaceObjectAtIndex:0 withObject:@"2"];
    [array removeObjectAtIndex:0];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    /*  change 字典的值为:
        {
            indexes:对应的值为数组操作的详细信息,包括索引等
            kind:   对应的值为数组操作的方式:
                     2:代表插入操作
                     3:代表删除操作
                     4:代表替换操作
                     typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
                         NSKeyValueChangeSetting = 1,
                         NSKeyValueChangeInsertion = 2,
                         NSKeyValueChangeRemoval = 3,
                         NSKeyValueChangeReplacement = 4,
                     };
            new/old:如果是插入操作,则字典中只会有new字段,对应的值为插入的元素,前提条件是options中传入了(NSKeyValueObservingOptionNew)
                     如果是删除操作,则字典中只会有old字段,对应的值为删除的元素,前提条件是options中传入了(NSKeyValueObservingOptionOld)
                     如果是替换操作,则字典中new和old字段都可以存在,对应的值为替换后的元素和替换前的元素,前提条件是options中传入了(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)

            indexes = "<_NSCachedIndexSet: 0x600001d092e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
            kind = 2; 
            new =     (
                1
            );
        }
     */  
    NSLog(@"%@",change);  
}

- (void)dealloc
{
    [self.person removeObserver:self forKeyPath:@"mArray"];
}
4.自动触发控制

可以在被观察对象的类中重写 + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key 方法来控制 KVO 的自动触发。

如果我们只允许外界观察 person 的 name 属性,可以在 Person 类如下操作。这样外界就只能观察 name 属性,即使外界注册了对 person 对象其它属性的监听,那么在属性发生改变时也不会触发 KVO。

// 返回值代表允不允许触发 KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    BOOL automatic = NO;
    if ([key isEqualToString:@"name"]) {
        automatic = YES;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}

也可以实现遵循命名规则为 + (BOOL)automaticallyNotifiesObserversOf 的方法来单一控制属性的 KVO 自动触发, 为属性名(首字母大写)。

+ (BOOL)automaticallyNotifiesObserversOfName
{
    return NO;
}
  • 第一个方法的优先级高于第二个方法。如果实现了 automaticallyNotifiesObserversForKey: 方法,并对 做了处理,则系统就不会再调用该 automaticallyNotifiesObserversOf 方法。
  • options 指定的 NSKeyValueObservingOptionInitial 触发的 KVO 通知,是无法被 automaticallyNotifiesObserversForKey: 阻止的。
5.手动触发

使用场景:
使用 KVO 监听成员变量值的改变;
在某些需要控制监听过程的场景下。比如:为了尽量减少不必要的触发通知操作,或者当多个更改同时具备的时候才调用属性改变的监听方法。

由于 KVO 的本质,重写 setter 方法来达到可以通知所有观察者对象的目的,所以只有通过 setter 方法或 KVC 方法去修改属性变量值的时候,才会触发 KVO,直接修改成员变量不会触发 KVO。
当我们要使用 KVO 监听成员变量值改变的时候,可以通过在为成员变量赋值的前后手动调用 willChangeValueForKey:didChangeValueForKey: 两个方法来手动触发 KVO,如:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    [self.person willChangeValueForKey:@"age"];
    self.person->_age = 18;
    [self.person didChangeValueForKey:@"age"];
}

NSKeyValueObservingOptionPrior(分别在值改变前后触发方法,即一次修改有两次触发)的两次触发分别在 willChangeValueForKey:didChangeValueForKey: 的时候进行的。
如果注册方法中 options 传入 NSKeyValueObservingOptionPrior,那么可以通过只调用 willChangeValueForKey: 来触发改变前的那次 KVO,可以用于在属性值即将更改前做一些操作。

6.新旧值相等时不触发

有时候我们可能会有这样的需求,KVO 监听的属性值修改前后相等的时候,不触发 KVO 的监听方法,可以结合 KVO 的自动触发控制和手动触发来实现。

例如:对 person 对象的 name 属性注册了 KVO 监听,我们希望在对 name 属性赋值时做一个判断,如果新值和旧值相等,则不触发 KVO,可以在 Person 类中如下这样实现,将 name 属性值改变的 KVO 触发方式由自动触发改为手动触发。

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

- (void)setName:(NSString *)name
{
    if (![_name isEqualToString:name]) {
        [self willChangeValueForKey:@"name"];
        _name = name;
        [self didChangeValueForKey:@"name"];
    } 
}
7.手动观察集合属性

有些情况下我们想手动观察集合属性,下面以观察数组为例。
关键方法:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

需要注意的是,根据 KVC 的 NSMutableArray 搜索模式:

  • 至少要实现一个插入和一个删除方法,否则不会触发 KVO。如
    插入方法:insertObject:inAtIndex:或insert:atIndexes:
    删除方法:removeObjectFromAtIndex:或removeAtIndexes:
  • 可以不实现替换方法,但是如果不实现替换方法,执行替换操作时,KVO 会把它当成先删除后添加,即会触发两次 KVO。第一次触发的 KVO 中 change 字典的 old 键的值为替换前的元素,第二次触发的 KVO 中 change 字典的 new 键的值为替换后的元素,前提条件是注册方法中的 options 传入对应的枚举值。
  • 如果实现替换方法,则执行替换操作只会触发一次 KVO,并且 change 字典会同时包含 newold,前提条件是注册方法中的 options 传入对应的枚举值。
    替换方法:replaceObjectInAtIndex:withObject:replaceAtIndexes:with:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    BOOL automatic = NO;
    if ([key isEqualToString:@"mArray"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}

- (void)insertMArray:(NSArray *)array atIndexes:(NSIndexSet *)indexes
{
    [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];

    [self.mArray insertObjects:array atIndexes:indexes];

    [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];
}

- (void)removeMArrayAtIndexes:(NSIndexSet *)indexes
{
    [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];

    [self.mArray removeObjectsAtIndexes:indexes];

    [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];
}

- (void)replaceMArrayAtIndexes:(NSIndexSet *)indexes withMArray:(NSArray *)array
{
    [self willChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];

    [self.mArray replaceObjectsAtIndexes:indexes withObjects:array];

    [self didChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];
}
8.依赖观察
(1)一对一关系

有些情况下,一个属性的改变依赖于别的一个或多个属性的改变,也就是说当别的属性改了,这个属性也会跟着改变。
比如我们想要对 Download 类中的 downloadProgress 属性进行 KVO 监听,该属性的改变依赖于 writtenData 和 totalData 属性的改变。观察者监听了 downloadProgress ,当 writtenData 和 totalData 属性值改变时,观察者也应该被通知。以下有两种方法可以解决这个问题。

  • 重写以下方法来指明 downloadProgress 属性依赖于 writtenData 和 totalData:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"writtenData",@"totalData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
  • 实现一个遵循命名规则为 keyPathsForValuesAffecting 的类方法,是依赖于其他值的属性名(首字母大写):
+ (NSSet *)keyPathsForValuesAffectingDownloadProgress
{
    return [NSSet setWithObjects:@"writtenData",@"totalData", nil];
}

以上两个方法可以同时存在,且都会调用,但是最终结果会以 keyPathsForValuesAffectingValueForKey: 为准。

(2)一对多关系

以上方法在观察集合属性时就不管用了。例如,假如你有一个 Department 类,它有一个装有 Employee 类的实例对象的数组,Employee 类有 salary 属性。你希望 Department 类有一个 totalSalary 属性来计算所有员工的薪水,也就是在这个关系中 Department 的 totalSalary 依赖于所有 Employee 实例对象的 salary 属性。以下有两种方法可以解决这个问题。

  • 你可以用 KVO 将 parent(比如 Department )作为所有 children(比如 Employee )相关属性的观察者。你必须在把 child 添加或删除到 parent 时把 parent 作为 child 的观察者添加或删除。在 observeValueForKeyPath:ofObject:change:context: 方法中我们可以针对被依赖项的变更来更新依赖项的值:
#import "Department.h"

static void *totalSalaryContext = &totalSalaryContext;

@interface Department ()
@property (nonatomic,strong)NSArray *employees;
@property (nonatomic,strong)NSNumber *totalSalary;

@end


@implementation Department

- (instancetype)initWithEmployees:(NSArray *)employees
{
    self = [super init];
    if (self) {
        self.employees = [employees copy];
        for (Employee *em in self.employees) {
            [em addObserver:self forKeyPath:@"salary" options:NSKeyValueObservingOptionNew context:totalSalaryContext];
        }
    }
    return self;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == totalSalaryContext) {
        [self setTotalSalary:[self valueForKeyPath:@"[email protected]"]];
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    
}
 
- (void)setTotalSalary:(NSNumber *)totalSalary
{
    if (_totalSalary != totalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = totalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}

- (void)dealloc
{
    for (Employee *em in self.employees) {
        [em removeObserver:self forKeyPath:@"salary" context:totalSalaryContext];
    }
}

@end
  • 使用 iOS 中观察者模式的另一种实现方式:通知(NSNotification)。

四、使用注意

1.移除观察者的注意点
  • 在调用 KVO 注册方法后,KVO 并不会对观察者进行强引用,所以需要注意观察者的生命周期。至少需要在观察者销毁之前,调用 KVO 移除方法移除观察者,否则如果在观察者被释放后,再次触发 KVO 监听方法就会导致 Crash。
  • KVO 的注册方法和移除方法应该是成对的,如果重复调用移除方法,就会抛出异常 NSRangeException 并导致程序 Crash。
  • 苹果官方推荐的方式是,在观察者初始化期间(init或者viewDidLoad的时候)注册为观察者,在释放过程中(dealloc时)调用移除方法,这样可以保证它们是成对出现的,是一种比较理想的使用方式。
2.防止多次注册和移除相同的 KVO

有时候我们难以避免多次注册和移除相同的 KVO,或者移除了一个未注册的观察者,从而产生可能会导致 Crash 的风险。
三种解决方案:黑科技防止多次添加删除KVO出现的问题

  • 利用 @try @catch(只能针对删除多次KVO的情况下) 给NSObject增加一个分类,然后利用Runtime API交换系统的removeObserver方法,在里面添加@try @catch
  • 利用 模型数组 进行存储记录;
  • 利用 observationInfo 里私有属性。
3.其它注意点
  • 如果对象被注册成为观察者,则该对象必须能响应监听方法,即该对象所属类中必须实现监听方法。当被观察对象属性发生改变时就会调用监听方法。如果没有实现就会导致 Crash。所以 KVO 三部曲缺一不可。
  • keyPath传入的是一个字符串,为避免写错,可以使用 NSStringFromSelector(@selector(propertyName)),将属性的 getter 方法 SEL 转换成字符串,在编译阶段对 keyPath 进行检验。
  • 如果注册方法中 context 传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问 context 就可能导致 Crash。
  • 如果是监听集合对象的改变,需要通过 KVC 的 mutableArrayValueForKey: 等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发 KVO。如果直接对集合对象进行操作改变,不会触发 KVO。
  • 在观察者类的监听方法中,应该为无法识别的 context 或者 objectkeyPath 调用父类的实现 [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];

五.KVO原理

1.NSKVONotifying_

我们在对象添加监听之前分别打印对象类型

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"监听前model类型object_getClass:%@",object_getClass(self.model));
    NSLog(@"监听前model类型class:%@",self.model.class);
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior;
    [self.model addObserver:self forKeyPath:@"name" options:options context:@"context"];
    NSLog(@"监听后model类型object_getClass:%@",object_getClass(self.model));
    NSLog(@"监听后model类型class:%@",self.model.class);
}
//打印结果
2021-12-21 10:21:39.709506+0800 DJTestTwo[6650:443575] 监听前model类型object_getClass:DJModel
2021-12-21 10:21:39.709661+0800 DJTestTwo[6650:443575] 监听前model类型class:DJModel
2021-12-21 10:21:39.710039+0800 DJTestTwo[6650:443575] 监听后model类型object_getClass:NSKVONotifying_DJModel
2021-12-21 10:21:39.710171+0800 DJTestTwo[6650:443575] 监听后model类型class:DJModel

我们看到,添加监听后,使用 object_getClass 方法获取model类型时获取到的是 NSKVONotifying_DJModel

这里就产生了几个问题:

  • 为什么添加监听后使用 object_getClass 获取到的对象类型是 NSKVONotifying_DJModel
    我们获取添加监听后的model对象的类对象的父类
- (void)viewDidLoad {
    [super viewDidLoad];
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior;
    [self.model addObserver:self forKeyPath:@"name" options:options context:@"context"];
    Class class = object_getClass(self.model);
    Class superClass = class_getSuperclass(class);
    NSLog(@"添加监听后model的类对象:%@",class);
    NSLog(@"%@的父类对象%@",class,superClass);
}
//打印结果
2021-12-21 10:32:50.040556+0800 DJTestTwo[6759:450098] 添加监听后model的类对象:NSKVONotifying_DJModel
2021-12-21 10:32:50.040773+0800 DJTestTwo[6759:450098] NSKVONotifying_DJModel的父类对象DJModel

从打印结果可以看出,NSKVONotifying_DJModelDJModel 的子类,说明我们添加了监听之后动态创建了一个 DJModel 的子类 NSKVONotifying_DJModel,并将对象 DJModel的类型更改为了 NSKVONotifying_DJModel

  • 为什么添加监听收使用 class 方法和 object_getClass 方法获取到的类型不一样?
    我们查看 classobject_getClass 的源码
此源码在runtim源码的Object.mm中
-(id)class {
  return (id)isa; 
}

+ (id)class {
  return self;
}
此源码在runtim源码的objc-class.mm中
Class object_getClass(id obj) {
  if (obj) return obj->getIsa();
  else return Nil;
}

我们从源码看出,实例对象调用 class 方法会返回 isa 指针,类对象调用 class 方法会返回自己,通过 object_getClass 方法获取对象的类型也会返回 isa 指针。从源码上看model对象添加监听之后使用 class 和使用 object_getClass 方法获取到的类型应该是一样的,但是这里却不同,我们猜测在添加了监听之后在 NSKVONotifying_DJModel 中重写了 class 方法。
我们打印一下添加监听前后 class 方法的 IMP 地址来确认是否重写了class 方法。

- (void)viewDidLoad {
    [super viewDidLoad];
    Class class1 = object_getClass(self.model);
    NSLog(@"监听前model类型object_getClass:%@",object_getClass(class1));
    NSLog(@"监听前model的class实现地址:%p",method_getImplementation(class_getInstanceMethod(class1, @selector(class))));
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior;
    [self.model addObserver:self forKeyPath:@"name" options:options context:@"context"];
    Class class2 = object_getClass(self.model);
    NSLog(@"监听前model类型object_getClass:%@",object_getClass(class2));
    NSLog(@"监听前model的class实现地址:%p",method_getImplementation(class_getInstanceMethod(class2, @selector(class))));
}
//打印结果
2021-12-21 10:43:36.024516+0800 DJTestTwo[6825:456577] 监听前model类型object_getClass:DJModel
2021-12-21 10:43:36.024689+0800 DJTestTwo[6825:456577] 监听前model的class实现地址:0x10e267c9b
2021-12-21 10:43:36.025042+0800 DJTestTwo[6825:456577] 监听前model类型object_getClass:NSKVONotifying_DJModel
2021-12-21 10:43:36.025145+0800 DJTestTwo[6825:456577] 监听前model的class实现地址:0x10dd4e662

从打印结果可以看出,添加监听之后 class 方法的地址改变了,这验证了我们之前的猜想,NSKVONotifying_DJModel 类中重写了 class 方法。

我们监听对象时调用了 set 方法,我们对监听前后的 set 方法单独分析。
我们再添加监听前后分别打印 setName 方法的 IMP地址。

- (void)viewDidLoad {
    [super viewDidLoad];
    Class class1 = object_getClass(self.model);
    NSLog(@"监听前model类型object_getClass:%@",object_getClass(class1));
    IMP imp1 = method_getImplementation(class_getInstanceMethod(class1, @selector(setName:)));
    NSLog(@"监听前model的class实现地址:%p",imp1);
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior;
    [self.model addObserver:self forKeyPath:@"name" options:options context:@"context"];
    Class class2 = object_getClass(self.model);
    NSLog(@"监听后model类型object_getClass:%@",object_getClass(class2));
    IMP imp2 = method_getImplementation(class_getInstanceMethod(class2, @selector(setName:)));
    NSLog(@"监听后model的class实现地址:%p",imp2);
}
//打印结果
2021-12-21 10:48:53.412458+0800 DJTestTwo[6876:460439] 监听前model类型object_getClass:DJModel
2021-12-21 10:48:53.412632+0800 DJTestTwo[6876:460439] 监听前model的class实现地址:0x1023357b0
2021-12-21 10:48:53.413003+0800 DJTestTwo[6876:460439] 监听后model类型object_getClass:NSKVONotifying_DJModel
2021-12-21 10:48:53.413131+0800 DJTestTwo[6876:460439] 监听后model的class实现地址:0x102648b57

通过打印结果可以看出 setName 方法也在 NSKVONotifying_DJModel 中被重写了,我们再使用lldb来看下 setName 具体是什么

(lldb) print (IMP)0x10ba7b7b0
(IMP) $1 = 0x000000010ba7b7b0 (DJTestTwo`-[DJModel setName:] at DJModel.h:13)
(lldb) print (IMP)0x10bd8eb57
(IMP) $2 = 0x000000010bd8eb57 (Foundation`_NSSetObjectValueAndNotify)

第一个地址打印的是添加监听前 setName 方法的 IMP 地址,第二个打印的是添加监听后 setName 方法的 IMP 地址。
这里看出添加监听前 setName 对应的具体方法就是 setName ,但是添加监听后,setName 对应的鸡头方法却变成了 _NSSetObjectValueAndNotify 函数。
下面我们就来研究一下 _NSSetObjectValueAndNotify 函数。

2._NSSetObjectValueAndNotify
__NSSetBoolValueAndNotify
__NSSetCharValueAndNotify
__NSSetDoubleValueAndNotify
__NSSetFloatValueAndNotify
__NSSetIntValueAndNotify
__NSSetLongLongValueAndNotify
__NSSetLongValueAndNotify
__NSSetObjectValueAndNotify
__NSSetPointValueAndNotify
__NSSetRangeValueAndNotify
__NSSetRectValueAndNotify
__NSSetShortValueAndNotify
__NSSetSizeValueAndNotify
__NSSetUnsignedCharValueAndNotify
__NSSetUnsignedIntValueAndNotify
__NSSetUnsignedLongLongValueAndNotify
__NSSetUnsignedLongValueAndNotify
__NSSetUnsignedShortValueAndNotify

从上面与KVO相关的方法中我们可以看出,每一种数据类型都对应了一个 setXXXValueAndNotify 函数。
不过这些函数的具体实现没有公布,所以内部构造这里还是不清楚。
但是我们知道,在调用 `setXXXValueAndNotify 函数的过程中会调用另外两个方法。

- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key

测试后得出了以下几个结论:

  • 如果在创建监听的时候只使用了NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld 这两个枚举值,那么 observeValueForKeyPath 方法会在 didChangeValueForKey 方法调用后被调用。
  • 如果在创建监听的时候使用了 NSKeyValueObservingOptionPrior 枚举值,那么 observeValueForKeyPath 方法会在 willChangeValueForKey 方法调用后被调用第一次,在 didChangeValueForKey 方法调用后被调用第二次。
  • 如果在创建监听的时候使用了 NSKeyValueObservingOptionInitial 枚举值,那么在 observeValueForKeyPath 方法会在 willChangeValueForKey 方法调用之前被调用一次。

我们还可以利用这两个方法手动触发 observeValueForKeyPath 方法:

  • 当使用了 NSKeyValueObservingOptionInitial 枚举值时,创建监听时就会调用一次 observeValueForKeyPath 方法,不需要其他条件触发。
  • 当使用 NSKeyValueObservingOptionPrior 枚举值时,手动调用 willChangeValueForKey 时可以触发一次 observeValueForKeyPath 方法的调用。
  • 如果想在 didChangeValueForKey 方法调用后再调用一次 observeValueForKeyPath 方法,需要同时实现 willChangeValueForKeydidChangeValueForKey 两个方法才行。

所以我们判断在 _NSSetObjectValueAndNotify 函数内部,在调用原来的 set 方法之前插入了 willChangeValueForKey 方法,在调用原来的 set 方法之后插入了 didChangeValueForKey 方法,并根据初始化时的枚举值决定调用 observeValueForKeyPath 的时机。

3.总结

(1)添加监听时,会动态创建一个监听对象类型的子类,并将监听对象的 isa 指针指向新的子类。
(2)子类中重写了 class 和监听属性的 set 方法。
(3)重写 class 方法是为了不将动态创建的类型暴露出来。
(4)重写 set 方法是将 set 方法的具体实现替换成了与属性类型相关的 __NSSetXXXValueAndNotify 函数。
(5)在 __NSSetXXXValueAndNotify 函数内部在 set 方法前后分别插入了 willChangeValueForKeydidChangeValueForKey 这两个方法。
(6)根据添加监听时的枚举值决定调用 observeValueForKeyPath 的具体时机。

你可能感兴趣的:(iOS 设计模式(五)-KVO 详解)