iOS九阴真经:二十、KVO 原理探索和自定义 KVO

我正在参与掘金创作者训练营第 4 期,点击了解活动详情,马上报名一起学习吧!

一、KVO 简介

键值观察(Key-Value Observing)是一种机制,它允许对象在其他对象的指定属性发生更改时得到通知。

1. 注册观察者

要使用键值观察首先要注册观察者,可以通过 addObserver:forKeyPath:options:context:来注册观察者。

参数说明:

  • observer:观察者。

  • keyPath:要观察的属性。

  • options:NSKeyValueObservingOptionOld 观察属性改变之前的值;NSKeyValueObservingOptionNew 观察属性改变之后的值;NSKeyValueObservingOptionInitial 在观察者注册方法甚至返回之前立即向观察者发送通知;NSKeyValueObservingOptionPrior 在属性改变之后会发送两次通知,有一次通知始终带着 notificationIsPrior 这个 key。

  • context:上下文,一般情况下传 NULL。可以通过指定上下文的方式来区分搜到通知的观察者是父类还是子类。

例如,我们需要观察 student 的 age 属性变化后的旧值和新值:

[self.student addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
复制代码

我们也可以通过上下文来区分接收的对象是 student 还是 teacher:

static void *StudentAgeContext = &StudentAgeContext;
static void *TeacherAgeContext = &TeacherAgeContext;
复制代码
[self.person addObserver:self forKeyPath:@"score" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:StudentAgeContext];

[self.person addObserver:self forKeyPath:@"course" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:TeacherAgeContext];
复制代码

2. 接收更改通知

注册观察者后,观察者需要实现 observeValueForKeyPath:ofObject:change:context: 方法来接收被观察者更改后的通知。

参数说明:

  • keyPath:要观察的属性,和注册观察者方法中的 keyPath 相对应。

  • object:键路径 keyPath 的源对象。

  • change:一个字典,描述了对相对于对象的键路径 keyPath 处的属性值所做的更改。条目在更改字典键中进行了描述。和注册观察者方法中的 options 有关。

  • context:上下文,和注册观察者方法中的 context 相对应。

例如在观察者中实现 observeValueForKeyPath:ofObject:change:context: 方法并打印结果:

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

3. 作为观察者移除对象

通过 removeObserver:forKeyPath:context: 方法向被观察对象发送消息、指定观察对象、键路径和上下文来删除键值观察者。

参数说明:

  • observer:观察者,和注册观察者中的 observer 相对应。

  • keyPath:要观察的属性,和注册观察者中的 keyPath 相对应。

  • context:上下文,和注册观察者中的 context 相对应。

如果没有指定上下文,可以直接通过 removeObserver:forKeyPath: 方法来移除观察者,例如:

[self.student removeObserver:self forKeyPath:@"age"];
复制代码

收到 removeObserver:forKeyPath:context: 消息后,观察对象将不再收到 observeValueForKeyPath:ofObject:change:context: 指定键路径和对象的任何消息。

移除观察者的注意点:

  • 如果尚未注册为观察者,则要求将其作为观察者移除会导致 NSRangeException。

  • addObserver:forKeyPath:options:context:removeObserver:forKeyPath:context: 必须一一对应。

  • 可以通过 try/catch 来处理潜在的异常,防止移除观察者的时候导致 NSRangeException。

try/catch 的处理如下:

@try {
    [self.student removeObserver:self forKeyPath:@"age"];
} @catch (NSException *exception) {
    NSLog(@"\n------\nname:%@,\nreason:%@,\nuserInfo:%@------", exception.name, exception.reason, exception.userInfo);
} @finally {
    NSLog(@"不管是否抛出异常,都会执行");
}
复制代码

二、自动&手动更改通知

1. 自动更改通知

NSObject 提供了自动键值更改通知的基本实现。自动键值更改通知通知观察者使用键值兼容访问器以及键值编码方法所做的更改。

// 调用访问器方法。
[account setName:@"Savings"];

// 使用 setValue:forKey:
[account setValue:@"Savings" forKey:@"name"];

// 使用密钥路径,其中 'account' 是 'document' 的 kvc 兼容属性。
[document setValue:@"Savings" forKeyPath:@"account.name"];

// 使用 mutableArrayValueForKey: 检索关系代理对象。
/*
在平时的开发中,我们也许有需要去观察可变数组元素的变化,在给可变数组添加观察者之后,如果用常规的方式修改可变数组,observeValueForKeyPath:ofObject:change:context: 方法无法接收到改变后的通知。

此时我们可以通过 KVC 中介绍的 mutableArrayValueForKey: 方法来对数组进行增删查改,代码如下:
*/
NSMutableArray *courses = [self.student mutableArrayValueForKey:@"courses"];
[courses removeObjectAtIndex:5];
[courses insertObject:@"biology" atIndex:5];
复制代码

2. 手动更改通知

在某些情况下,你可能希望控制通知过程,例如,尽量减少因应用程序特定原因而不必要的触发通知,或将多个更改组合到单个通知中。手动更改通知提供了执行此操作的方法。

手动和自动通知并不相互排斥。除了已经存在的自动通知之外,你还可以自由发布手动通知。更典型的是,你可能希望完全控制特定属性的通知。在这种情况下,你将覆盖 NSObject。

automaticallyNotifiesObserversForKey: 对于您想要排除其自动通知的属性,automaticallyNotifiesObserversForKey: 应返回的子类实现 NO。子类实现应该为任何无法识别的键调用 super。

重写 automaticallyNotifiesObserversForKey: 方法的代码如下:

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

在更改 name 的值的时候手动更改 age 的值并且通知给观察者,代码如下:

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    [self willChangeValueForKey:@"age"];
    _name = name;
    _age = _age += 1;
    [self didChangeValueForKey:@"name"];
    [self didChangeValueForKey:@"age"];
}
复制代码

可变容器添加手动 KVO,可变容器内部元素发生添加,移除,替换时触发 KVO,代码如下:

// 可变容器添加手动kvo,可变容器内部元素发生添加,移除,替换时触发kvo
// 插入单个 元素
- (void)insertObject:(NSString *)object inCoursesAtIndex:(NSUInteger)index{
    [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"courses"];
    [_courses insertObject:object atIndex:index];
    [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"courses"];
}

// 删除 单个元素
- (void)removeObjectFromCoursesAtIndex:(NSUInteger)index{
    [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"courses"];
    [_courses removeObjectAtIndex:index];
    [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"courses"];
}
// 插入 多个元素
- (void)insertCourses:(NSArray *)array atIndexes:(NSIndexSet *)indexes{
    [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"courses"];
    [_courses insertObjects:array atIndexes:indexes];
    [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"courses"];
}
// 删除多个元素
- (void)removeCoursesAtIndexes:(NSIndexSet *)indexes{
    [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"courses"];
    [_courses removeObjectsAtIndexes:indexes];
    [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"courses"];
}
// 替换 单个
- (void)replaceObjectInCoursesAtIndex:(NSUInteger)index withObject:(id)object{
    [self willChange:NSKeyValueChangeReplacement valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"courses"];
    [_courses replaceObjectAtIndex:index withObject:object];
    [self didChange:NSKeyValueChangeReplacement valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"courses"];
}
// 替换 多个
- (void)replaceCoursesAtIndexes:(NSIndexSet *)indexes withCourses:(NSArray *)array{
    [self willChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"courses"];
    [_courses replaceObjectsAtIndexes:indexes withObjects:array];
    [self didChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"courses"];
}
复制代码

三、注册依赖键

在很多情况下,一个属性的值取决于另一个对象中的一个或多个其他属性的值。如果一个属性的值发生变化,那么派生属性的值也应该被标记为变化。如何确保为这些依赖属性发布键值观察通知取决于关系的基数。

要为一对一关系自动触发通知,你应该覆盖 keyPathsForValuesAffectingValueForKey: 或实现一个合适的方法,该方法遵循它为注册相关键定义的模式。

例如,一个人的全名取决于名字和姓氏。返回全名的方法可以写成如下:

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
复制代码

当 firstName 或 lastName 属性更改时,必须通知观察 fullName 属性的应用程序,因为它们会影响属性的值

一种解决方案是覆盖 keyPathsForValuesAffectingValueForKey:指定一个人的 fullName 属性依赖于 lastName 和 firstName 属性。

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

您的覆盖通常应该调用 super 并返回一个集合,该集合包括该集合中的任何成员(以免干扰超类中此方法的覆盖)。

您还可以通过实现遵循命名约定 keyPathsForValuesAffecting<Key> 的类方法来实现相同的结果,其中 <Key> 是依赖于值的属性的名称(首字母大写)。 使用这种模式,上面的代码可以重写命名为 keyPathsForValuesAffectingFullName 的类方法,代码如下:

+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
复制代码

当使用类别将计算属性添加到现有类时,你不能覆盖 keyPathsForValuesAffectingValueForKey: 方法,因为你不应该覆盖类别中的方法。在这种情况下,实现一个匹配的 keyPathsForValuesAffecting<Key> 类方法来利用这个机制。

四、KVO 原理

自动键值观察是使用一种称为 isa-swizzling 的技术实现的。顾名思义,isa 指针指向维护调度表的对象的类。 该调度表主要包含指向类实现的方法的指针,以及其他数据。当观察者为对象的属性注册时,被观察对象的 isa 指针被修改,指向中间类而不是真正的类。

接下来我们看一下这个中间类是什么,它在添加 KVO 之后都干了什么,代码如下:

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

/**打印对象的一些信息,验证*/
- (void)printObjectInfo;
@end
复制代码

先定义一个 Person 对象,这个 Person 有两个属性:name,age。并且添加了 printObjectInfo 方法用来打印类的一些信息,方法的实现如下:

- (void)printObjectInfo {
    NSLog(@"-------");
    NSLog(@"对象: %@, 地址: %p", self, &self);
    
    Class cls_object = object_getClass(self); // 类对象(person->isa)
    Class super_cls_object = class_getSuperclass(cls_object); // 类对象的父类对象(person->superclass_isa)
    Class meta_cls_object = object_getClass(cls_object); // 元类对象(person->isa->isa)
    NSLog(@"class 对象: %@", cls_object);
    NSLog(@"class 对象的 superclass 对象: %@", super_cls_object);
    NSLog(@"metaclass 对象: %@", meta_cls_object);
    
    IMP name_imp = [self methodForSelector:@selector(setName:)];
    IMP age_imp = [self methodForSelector:@selector(setAge:)];
    NSLog(@"setName: %p, setAge: %p", name_imp, age_imp);
    
    [self printMethodNamesOfClass:cls_object];
}

- (void)printMethodNamesOfClass:(Class)cls {
    unsigned int count;
    Method *methodList = class_copyMethodList(cls, &count);
    NSMutableArray<NSString *> *methodNames = [NSMutableArray<NSString *> array];
    for (int i = 0; i < count; i++) {
        Method method = methodList[i];
        SEL selector = method_getName(method);
        NSString *methodName = NSStringFromSelector(selector);
        [methodNames addObject:methodName];
    }
    
    free(methodList);
    
    NSLog(@"对象的方法列表:%@", methodNames);
}
复制代码

接下来我们通过调用 printObjectInfo 来观察属性 name 未添加观察者和添加观察者后 Person 的变化。

NSLog(@"添加 Observer 之前");
[self.p1 printObjectInfo];
    
[self.p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    
NSLog(@"p1 添加 Observer 之后");
[self.p1 printObjectInfo];
复制代码
打印如下:
2022-02-28 18:41:02.916091+0800 01-KVO简介[9164:181067] 添加 Observer 之前
2022-02-28 18:41:02.916244+0800 01-KVO简介[9164:181067] -------
2022-02-28 18:41:02.916376+0800 01-KVO简介[9164:181067] 对象: <Person: 0x6000020bc2e0>, 地址: 0x7ff7bbd55ea8
2022-02-28 18:41:02.916512+0800 01-KVO简介[9164:181067] class 对象: Person
2022-02-28 18:41:02.916623+0800 01-KVO简介[9164:181067] class 对象的 superclass 对象: NSObject
2022-02-28 18:41:02.916747+0800 01-KVO简介[9164:181067] metaclass 对象: Person
2022-02-28 18:41:02.916839+0800 01-KVO简介[9164:181067] setName: 0x1041ab5c0, setAge: 0x1041ab970
2022-02-28 18:41:02.916991+0800 01-KVO简介[9164:181067] 对象的方法列表:(
    printObjectInfo,
    "printMethodNamesOfClass:",
    name,
    ".cxx_destruct",
    "setName:",
    "willChangeValueForKey:",
    "didChangeValueForKey:",
    age,
    "setAge:"
)
2022-02-28 18:41:02.917303+0800 01-KVO简介[9164:181067] p1 添加 Observer 之后
2022-02-28 18:41:02.917413+0800 01-KVO简介[9164:181067] -------
2022-02-28 18:41:02.917519+0800 01-KVO简介[9164:181067] 对象: <Person: 0x6000020bc2e0>, 地址: 0x7ff7bbd55ea8
2022-02-28 18:41:02.954703+0800 01-KVO简介[9164:181067] class 对象: NSKVONotifying_Person
2022-02-28 18:41:02.954829+0800 01-KVO简介[9164:181067] class 对象的 superclass 对象: Person
2022-02-28 18:41:02.954933+0800 01-KVO简介[9164:181067] metaclass 对象: NSKVONotifying_Person
2022-02-28 18:41:02.955019+0800 01-KVO简介[9164:181067] setName: 0x7fff207a3203, setAge: 0x1041ab970
2022-02-28 18:41:02.955111+0800 01-KVO简介[9164:181067] 对象的方法列表:(
    "setName:",
    class,
    dealloc,
    "_isKVOA"
)
复制代码

通过打印发现,在添加观察者之后,Person 对象的类对象类对象的父类元类对象以及 setName: 方法的地址发生了变化,并且,对象的方法列表也随之发生了变化。

添加观察者模式后类对象和元类对象都变成了 NSKVONotifying_Person,它是 Person 的子类。由此可见,当观察者为对象的属性注册时,被观察对象的 isa 指针被修改,指向中间类(该类通常以 NSKVONotifying_<className> 的方式命名)。

所以,所谓的“指向中间类”本质上就是把 isa 指向的类对象和元类对象换成了动态生成的 NSKVONotifying_Person。并且,在 NSKVONotifying_Person 里实现 KVO 相关的代码。

接下来我们看一下添加观察者模式之后的 setName: 本质的变化是什么,通过 IMP 可以把当前函数的地址还原,如下:

(lldb) po IMP(0x7fff207a3203)
(Foundation`_NSSetObjectValueAndNotify)
复制代码

此时发现属性 setter 方法的本质是一个 Foundation 中的 _NSSetObjectValueAndNotify 方法。那什么时候调用 _NSSetObjectValueAndNotify 方法呢?

在 Person 实现 setName:willChangeValueForKey:didChangeValueForKey:。通过打印观察其调用流程,代码如下:

- (void)setName:(NSString *)name {
    _name = name;
    NSLog(@"%s", __func__);
}

- (void)willChangeValueForKey:(NSString *)key {
    NSLog(@"%s", __func__);
    [super willChangeValueForKey:key];
}

- (void)didChangeValueForKey:(NSString *)key {
    NSLog(@"%s", __func__);
    [super didChangeValueForKey:key];
}
复制代码
打印结果:
2022-02-28 19:00:31.104707+0800 01-KVO简介[9433:187154] -[Person willChangeValueForKey:]
2022-02-28 19:00:31.104915+0800 01-KVO简介[9433:187154] -[Person setName:]
2022-02-28 19:00:31.105060+0800 01-KVO简介[9433:187154] -[Person didChangeValueForKey:]
-------
keyPath:name,
object:<Person: 0x6000005693c0>,
change:{
    kind = 1;
    new = "zhang shan";
    old = "zhang shan";
},
context:(null)
-------
复制代码

当修改实例对象的属性值时,动态生成的子类的 _NSSetObjectValueAndNotify 首先调用 willChangeValueForKey:,接着调用父类的 setName:,最后调用 didChangeValueForKey:,随后触发 observeValueForKeyPath:ofObject:change:context: 方法。

根据属性的类型不同,动态生成的子类方法的名称也不相同,比如 NSString 类型的为 _NSSetObjectValueAndNotify,int 类型的为 _NSSetIntValueAndNotify。所以动态子类的 setter 方法的命名规则为:_NSSet<xxx>ValueAndNotify

除了被观察的属性的 setter 被重写之外,还有 class 方法和 dealloc 方法也被重写了,并且,动态子类还为自己添加了一个 _isKVOA 方法。

  • 重写 class 方法:猜测内部的实现应该类似 return class_getSuperclass(object_getClass(self)) 这种,所以才会在添加观察者模式后调用实例方法 class 时返回的和 object_getClass 返回的类对象不一致,其原因可能是为了屏蔽内部实现,让开发者不要多想,用就行了。

  • 重写 dealloc 方法:做一些收尾工作,比如将 isa 指针指回 Person。

  • _isKVOA:会新生成的一个 _isKVOA 方法。内部实现应该是 return YES,作为使用了 KVO 的标记。

当移除观察者之后,自动生成的子类是否会被销毁呢?我们通过一段代码来验证一下,代码如下:

- (void)printMethodNamesOfClass:(Class)cls {
    unsigned int count;
    Method *methodList = class_copyMethodList(cls, &count);
    NSMutableArray<NSString *> *methodNames = [NSMutableArray<NSString *> array];
    for (int i = 0; i < count; i++) {
        Method method = methodList[i];
        SEL selector = method_getName(method);
        NSString *methodName = NSStringFromSelector(selector);
        [methodNames addObject:methodName];
    }
    
    free(methodList);
    
    NSLog(@"对象的方法列表:%@", methodNames);
}
复制代码

移除观察者的代码和调用 printMethodNamesOfClass: 方法的代码如下:

[self.p1 removeObserver:self forKeyPath:@"name"];
NSLog(@"p1 移除 Observer 之后");
[self.p1 printObjectInfo];
    
NSLog(@"检查 NSKVONotifying_Person 是否被释放");
Class cls = objc_getClass("NSKVONotifying_Person");
[self printMethodNamesOfClass:cls];
NSLog(@"检查结束");
复制代码
打印结果:
2022-02-28 19:43:36.033839+0800 01-KVO简介[10944:219671] p1 移除 Observer 之后
2022-02-28 19:43:36.033980+0800 01-KVO简介[10944:219671] -------
2022-02-28 19:43:36.034110+0800 01-KVO简介[10944:219671] 对象: <Person: 0x6000033c6fe0>, 地址: 0x7ff7b123f4a8
2022-02-28 19:43:36.034267+0800 01-KVO简介[10944:219671] class 对象: Person
2022-02-28 19:43:36.034409+0800 01-KVO简介[10944:219671] class 对象的 superclass 对象: NSObject
2022-02-28 19:43:36.034529+0800 01-KVO简介[10944:219671] metaclass 对象: Person
2022-02-28 19:43:36.034760+0800 01-KVO简介[10944:219671] setName: 0x10ecc15c0, setAge: 0x10ecc1970
2022-02-28 19:43:36.035107+0800 01-KVO简介[10944:219671] 对象的方法列表:(
    printObjectInfo,
    "printMethodNamesOfClass:",
    name,
    ".cxx_destruct",
    "setName:",
    "willChangeValueForKey:",
    "didChangeValueForKey:",
    age,
    "setAge:"
)
2022-02-28 19:43:36.035249+0800 01-KVO简介[10944:219671] 检查 NSKVONotifying_Person 是否被释放
2022-02-28 19:43:36.035374+0800 01-KVO简介[10944:219671] 对象的方法列表:(
    "setName:",
    class,
    dealloc,
    "_isKVOA"
)
2022-02-28 19:43:36.035534+0800 01-KVO简介[10944:219671] 检查结束
2022-02-28 19:43:36.035731+0800 01-KVO简介[10944:219671] 不管是否抛出异常,都会执行
复制代码

通过打印结果得知,在移除观察者之后,实例对象 isa 和方法会恢复原样。但是我们手动的获取 NSKVONotifying_Person 对象的时候,还是能获取到 NSKVONotifying_Person 的方法列表。

说明添加观察者之后动态生成的子类不会随着观察者的移除而销毁,而是将其缓存起来,目的是为了下次再使用的时候,避免重新生成,减少性能的开销

五、自定义 KVO

接下来我们通过自定义 KVO 来加深 KVO 的底层原理,首先模仿系统的三个方法,在 NSObject 的分类中添加三个方法:

/// 自定义KVO添加观察者方法
- (void)sh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable )context;

/// 自定义移除观察者
- (void)sh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

/// 自定义KVO观察者方法
- (void)sh_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void * _Nullable)context;
复制代码

1. 自定义添加观察者

自定义添加观察者的思路:

  1. 验证 keyPath 是否有 setter 的实例方法。

  2. 动态生成子类。

    a). 拼接子类名称,模仿系统的拼接规则(NSKVONotifying_<className>)。
    b). 根据通过 NSClassFromString 方法获取类名称获取对应的 Class。
    c). 判断获取的 Class 是否为 nil,如果为 nil,则向系统申请并注册类,并且添加 setter、classdealloc_isKVOA 方法。

  3. 将 isa 指向动态生成的子类。

  4. 缓存观察者。

    用关联对象并通过数组对 sh_addObserver:forKeyPath:options:context: 传过来的参数进行缓存。

代码如下:

- (void)sh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable)context {
    // 1. setter 方法验证
    if (sh_judgeSetterMethodWithClass(object_getClass(self), keyPath) == NO) {
        NSLog(@"没有相应的 setter");
        return;
    }
    
    // 2. 动态生成子类
    Class subclass_kvo = [self sh_createChildClassWithKeyPath:keyPath];
    
    // 3. isa的指向 : SHKVONotifying_xxx
    object_setClass(self, subclass_kvo);
    
    // 4. 保存观察者
    NSMutableArray *kvoInfos = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(SHKVOAssociatedObjectKey));
    
    if (kvoInfos == nil) {
        kvoInfos = [NSMutableArray arrayWithCapacity:1];
    }
    
    SHKVOInfo *kvoInfo = [[SHKVOInfo alloc] initWithObserver:observer keyPath:keyPath options:options];
    [kvoInfos addObject:kvoInfo];
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(SHKVOAssociatedObjectKey), kvoInfos, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
复制代码

验证 keyPath 是否有相应的 setter 的方法如下:

/// 验证 class 对象是否有 keyPath 对应的 setter 存在
/// @param cls class 对象
/// @param keyPath keyPath
static BOOL sh_judgeSetterMethodWithClass(Class cls, NSString *keyPath)
{
    // 根据 keyPath 拼接的 setter
    SEL sel = NSSelectorFromString(sh_setterForKeyPath(keyPath));
    // 检查 setterMthod 是否为 nil,如果没有 nil,则没有 setter
    Method setterMthod = class_getInstanceMethod(cls, sel);
    return !(setterMthod == nil);
}

/// 传一个 keyPath 返回一个 keyPath 对应的 setter
/// @param keyPath keyPath
static NSString *sh_setterForKeyPath(NSString *keyPath)
{
    // nil 判断
    if (keyPath.length <= 0) return nil;
    // 取首字母并且大写形式
    NSString *firstString = [[keyPath substringToIndex:1] uppercaseString];
    // 取首字母以外的字母
    NSString *leaveString = [keyPath substringFromIndex:1];
    
    return [NSString stringWithFormat:@"set%@%@:", firstString, leaveString];
}
复制代码

动态生成子类的方法如下:

/// 传一个 keyPath 动态的创建一个 当前类相关的 NSKVONotifying_xxx 类
/// @param keyPath keyPath
- (Class)sh_createChildClassWithKeyPath:(NSString *)keyPath {
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",SHKVONotifyingKey, oldClassName];
    Class newClass = NSClassFromString(newClassName);
    
    if (newClass == nil) {
        // 申请类
        // 第一个参数:父类
        // 第二个参数:申请类的名称
        // 第三个参数:开辟的额外空间
        newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
        
        // 添加 class 方法: class的指向是当前实例对象的 class 对象
        SEL classSel = NSSelectorFromString(@"class");
        Method classMethod = class_getInstanceMethod([self class], classSel);
        const char *classTypes = method_getTypeEncoding(classMethod);
        class_addMethod(newClass, classSel, (IMP)sh_kvo_class, classTypes);
        
        // 添加 dealloc 方法
        SEL deallocSel = NSSelectorFromString(@"dealloc");
        Method deallocMethod = class_getInstanceMethod([self class], deallocSel);
        const char *deallocTypes = method_getTypeEncoding(deallocMethod);
        class_addMethod(newClass, deallocSel, (IMP)sh_kvo_dealloc, deallocTypes);
        
        // 添加 _isKVOA 方法
        SEL _isKVOASel = NSSelectorFromString(@"_isKVOA");
        Method _isKVOAMethod = class_getInstanceMethod([self class], _isKVOASel);
        const char *_isKVOATypes = method_getTypeEncoding(_isKVOAMethod);
        class_addMethod(newClass, _isKVOASel, (IMP)sh_isKVOA, _isKVOATypes);
        
        // 注册类
        objc_registerClassPair(newClass);
    }
    
    // 添加 setter 方法
    SEL setterSel = NSSelectorFromString(sh_setterForKeyPath(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSel);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSel, (IMP)sh_kvo_setter, setterTypes);
    
    return newClass;
}
复制代码

SHKVONotifyingKey 的定义如下:

static NSString *const SHKVONotifyingKey = @"SHKVONotifying_";
复制代码

在动态生成子类我们也需要针对 classdealloc、和 _isKVOA 方法做一个处理,根据 KVO 的原理分析,这些方法的实现大致如下:

Class sh_kvo_class(id self)
{
    return class_getSuperclass(object_getClass(self));
}

void sh_kvo_dealloc(id self)
{
    // 把 isa 指回去
    object_setClass(self, sh_kvo_class(self));
}

BOOL sh_isKVOA(id self)
{
    return YES;
}
复制代码

动态的生成子类之后就是将当前对象的 isa 指向动态生成的子类,接下来就是将参数进行缓存。通过 SHKVOInfo 对象记录传进来的参数,接着通过关联对象的方式对 SHKVOInfo 的实例进行缓存。

关联对象需要定义一个 Key 来记录。

static NSString *const SHKVOAssociatedObjectKey = @"SHKVOAssociatedObjectKey";
复制代码

SHKVOInfo 的实现如下:

@interface SHKVOInfo : NSObject
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, weak) id observer;
@property (nonatomic) NSKeyValueObservingOptions options;

- (instancetype)initWithObserver:(id)observer keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options;
@end

@implementation SHKVOInfo
- (instancetype)initWithObserver:(id)observer keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options {
    self = [super init];
    if (self) {
        self.observer = observer;
        self.keyPath = keyPath;
        self.options = options;
    }
    return self;
}
@end
复制代码

至此,sh_addObserver:forKeyPath:options:context: 的实现就结束了。

2. 动态子类的 setter

在第 1 点的动态生成的子类中,需要给子类添加一个 setter,这个 setter 才是整个自定义 KVO 的核心点。相当于前面分析的 KVO 原理中 _NSSet<xxx>ValueAndNotify 方法的实现。

这个 setter 方法的思路大致如下:

  1. 获取 keyPath。
  2. 获取缓存的观察者数组。
  3. 将缓存里有 keyPath 的观察者对象取出。
  4. 调用 willChangeValueForKey: 方法。
  5. 核心重点!,发生消息给父类,相当于 [super setter]。
  6. 调用 didChangeValueForKey: 方法。
  7. 发送消息给观察者。

代码如下:

void sh_kvo_setter(id self, SEL _cmd, id newValue)
{
    NSString *keyPath = sh_getterForSetter(NSStringFromSelector(_cmd));
    
    // 查找观察者
    NSMutableArray *kvoInfos = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(SHKVOAssociatedObjectKey));
    
    // 遍历
    for (SHKVOInfo *kvoInfo in kvoInfos) {
        if ([kvoInfo.keyPath isEqualToString:keyPath]) {
            if (kvoInfo == nil) {
                [self sh_objc_msgSendSuper:_cmd newValue:newValue];
                return;
            }
            
            // 获取 change
            NSDictionary *change = [self sh_changeForKVOInfo:kvoInfo keyPath:keyPath newValue:newValue];
            
            // 调用 willChangeValueForKey
            [self willChangeValueForKey:keyPath];
            
            // 判断 自动开关 省略
            // 核心 -> Person - setter _cmd 父类发送消息
            [self sh_objc_msgSendSuper:_cmd newValue:newValue];
            
            
            // 调用 didChangeValueForKey
            [self didChangeValueForKey:keyPath];
            
            // 2.发送消息-观察者
            SEL observerSel = NSSelectorFromString(@"sh_observeValueForKeyPath:ofObject:change:context:");
            ((void (*)(id, SEL, NSString *, id, NSDictionary *, void *))objc_msgSend)(kvoInfo.observer, observerSel, keyPath, self, change, NULL);
        }
    }
}
复制代码

获取 keyPath 的方法如下:

/// 传一个 setter 方法名返回一个 setter 对应的 getter
/// @param setter setter
static NSString *sh_getterForSetter(NSString *setter)
{
    if (![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"] ||setter.length <= 3) {
        return nil;
    }
    
    NSRange range = NSMakeRange(3, setter.length-4);
    NSString *getter = [setter substringWithRange:range];
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return  [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}
复制代码

在取出缓存的观察者信息之后,就是根据分析的 KVO 原理的结果,依次的调用 willChangeValueForKey: 方法,父类的 setter 方法和 didChangeValueForKey: 方法,最后,将消息发送给观察者。

调用父类的 setter 方法的实现如下:

/// 给父类发送消息
/// @param sel sel
/// @param newValue newValue
- (void)sh_objc_msgSendSuper:(SEL)sel newValue:(id)newValue {
    struct objc_super sh_objc_super;
    sh_objc_super.receiver = self;
    sh_objc_super.super_class = sh_kvo_class(self);
    // 1.给父类发送消息
    ((void (*)(void *, SEL, id))objc_msgSendSuper)(&sh_objc_super, sel, newValue);
}
复制代码

在调用 sh_observeValueForKeyPath:ofObject:change:context: 方法通知观察者之前需要拿到 change 的信息,系统方法的 change 是一个字典。

我们也模仿系统的 change,生成 change 的方法如下:

/// 设置 change 并返回
/// @param kvoInfo kvoInfo
/// @param keyPath keyPath
/// @param newValue newValue
- (NSDictionary *)sh_changeForKVOInfo:(SHKVOInfo *)kvoInfo keyPath:(NSString *)keyPath newValue:(id)newValue {
    NSMutableDictionary *change = [NSMutableDictionary dictionary];
    
    if (kvoInfo.options & NSKeyValueObservingOptionOld) {
        id oldValue = [self valueForKey:keyPath];
        if (oldValue) {
            [change setObject:oldValue forKey:NSKeyValueChangeOldKey];
        }else {
            [change setObject:@"" forKey:NSKeyValueChangeOldKey];
        }
    }
    
    if (kvoInfo.options & NSKeyValueObservingOptionNew) {
        [change setObject:newValue forKey:NSKeyValueChangeNewKey];
    }
    
    return change.copy;
}
复制代码

最后,为了防止通知观察的时候由于没有实现 sh_observeValueForKeyPath:ofObject:change:context: 方法而导致崩溃,所以我们需要实现一个默认 sh_observeValueForKeyPath:ofObject:change:context: 方法,代码如下:

- (void)sh_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void * _Nullable)context {
}
复制代码

至此,动态子类的 setter 的大致实现就结束了。

3. 自定义移除观察者

自定义添加观察者和如何通知观察者实现了之后,剩下的就是自定义移除观察者了。

自定义移除观察者实现的思路大致如下:

  1. 获取缓存观察者的关联对象,做 nil 处理。
  2. 从数组中查找与 keyPath 相匹配的观察者,并删除。
  3. 如果数组中的 count 为 0,将 isa 指回。

接下来就是跟着思路来来一步一步的实现,代码如下:

- (void)sh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    // 获取关联数组对象
    NSMutableArray *kvoInfos = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(SHKVOAssociatedObjectKey));
    
    // nil 处理
    if (kvoInfos == nil) return;
    
    // 从数组中删除 keyPath 对应的 kvoInfo
    for (SHKVOInfo *kvoInfo in kvoInfos.copy) {
        if ([kvoInfo.keyPath isEqualToString:keyPath]) {
            [kvoInfos removeObject:kvoInfo];
            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(SHKVOAssociatedObjectKey), kvoInfos, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            break;
        }
    }
    
    // 如果数组中没有了 kvoInfo, 将 isa 指回父类
    if (kvoInfos.count <= 0) {
        object_setClass(self, sh_kvo_class(self));
    }
}
复制代码

至此,整个自定义 KVO 的实现就基本的完成了,其实这个过程中很多的细节并没有处理,比如处理属性的不同类型,这个自定义 KVO 只是为了加深对 KVO 原理的理解,这里就不去实现过细的功能了。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享