一、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 原理的理解,这里就不去完成过细的功能了。