iOS 底层探索 - KVO

什么是 KVO?

KVO 提供了一种当其他对象的属性发生变化就会通知观察者对象的机制

根据官网的定义,属性的分类可以分为下列三种:

  • Attributes: 简单属性,比如基本数据类型,字符串和布尔值,而诸如 NSNumber 和其它一些不可变类型比如 NSColor
    也可以被认为是简单属性
  • To-one relationships: 这些是具有自己属性的可变对象属性。即对象的属性可以更改,而无需更改对象本身。例如,一个
    Account 对象可能具有一个 owner 属性,该属性是 Person 对象的实例,而 Person 对象本身具有 address
    属性。owner 的地址可以更改,但却而无需更改 Account 持有的 owner 属性。也就是说 Account 的 owner
    属性未被更改,只是 address 被更改了。
  • To-many relationships: 这些是集合对象属性。尽管也可以使用自定义集合类,但是通常使用 NSArray 或 NSSet
    的实例来持有此集合。

KVO 三大流程解析

观察者注册

- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context

observer:注册 KVO 通知的对象。观察者必须实现 key-value observing 方法 observeValueForKeyPath:ofObject:change:context:。

keyPath:被观察者的属性的 keypath,相对于接受者,值不能是 nil。

options: NSKeyValueObservingOptions 的组合,它指定了观察通知中包含了什么

context:在 observeValueForKeyPath:ofObject:change:context: 传给 observer 参数的上下文

移除观察者

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

取消注册有两个方法,不过建议还是跟注册和通知两个流程统一,选用带有 context 参数的方法。

移除KVO通知的必要性

在官方文档中,针对KVO的移除有以下几点说明
请添加图片描述

  • 取消注册与注册是一对一的关系

一旦对某个对象上的属性注册了键值观察,可以选择在收到属性值变化后取消注册,也可以在观察者声明周期结束之前(比如:dealloc 方法)

取消注册,如果忘记调用取消注册方法,那么一旦观察者被销毁后,KVO 机制会给一个不存在的对象发送变化回调消息导致野指针错误。

野指针错误原因

  1. 由于第一次注册KVO观察者后没有移除,
  2. 再次进入界面,会导致第二次注册KVO观察者,导致KVO观察的重复注册,
  3. 而且第一次的通知对象还在内存中,没有进行释放,
  4. 此时接收到属性值变化的通知,会出现找不到原有的通知对象,只能找到现有的通知对象,
  5. 即第二次KVO注册的观察者,所以导致了类似野指针的崩溃,即一直保持着一个野通知,且一直在监听
  • 不能重复取消注册

取消注册也不能对同一个观察者重复多次,为了避免 crash,可以把取消注册的代码包裹在 try&catch 代码块中:

context讲解

官方文档
请添加图片描述

上下文context指针包含任意数据,这些数据将在相应的更改通知中传递回观察者

context为NULL

可以通过指定context为NULL,从而依靠keyPath即键路径字符串传来确定更改通知的来源,

问题

但是这种方法可能会导致对象的父类由于不同的原因也观察到相同的键路径而导致问题

context的作用

所以可以为每个观察到的keyPath创建一个不同的context,从而完全不需要进行字符串比较,从而提高性能以及代码的可读性

context的总结

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

context使用案例

不使用context,使用keyPath区分通知来源

//context的类型是 nullable void *,应该是NULL,而不是nil
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];


使用context区分通知来源

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

//注册观察者
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
    
    
//KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if (context == PersonNickContext) {
        NSLog(@"%@",change);
    }else if (context == PersonNameContext){
        NSLog(@"%@",change);
    }
}


KVO的自动触发与手动触发

-自动: 说属性值变化完全是由系统控制,我们只需要告诉系统监听什么属性,然后就直接等系统告诉我们就完事了

  • 手动:程序员自己去管理

如何控制手动或自动

需要修改类方法 automaticallyNotifiesObserversForKey: 的返回值,这个方法如果返回 YES 就是自动挡,返回 NO 就是手动挡

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

手动 KVO 触发方式

基本使用
- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    _balance = theBalance;
    [self didChangeValueForKey:@"balance"];
}

最朴素的手动 KVO 使用方法就是在属性值改变前对观察者发送 willChangeValueForKey 实例方法,在属性值改变之后对观察者发送 didChangeValueForKey 实例方法,参数都是所观察的键。

提高性能使用
- (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
}

为了性能最佳,可以在属性的 setter 中判断是否要执行 will + did:

一个属性的改变会影响到多个键
- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    [self willChangeValueForKey:@"itemChanged"];
    _balance = theBalance;
    _itemChanged = _itemChanged+1;
    [self didChangeValueForKey:@"itemChanged"];
    [self didChangeValueForKey:@"balance"];
}

KVO观察:一对多

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

keyPathsForValuesAffectingValueForKey:方法不支持包含一对多关系的 Key Path

以下载进度为例,比如目前有一个需求,需要根据总的下载量totalData 和当前下载量currentData 来计算当前的下载进度currentProcess,实现有两种方式

分别观察 总的下载量totalData 和当前下载量currentData 两个属性,当其中一个发生变化计算 当前下载进度currentProcess

//1、合二为一的观察方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"currentProcess"]) {
        NSArray *affectingKeys = @[@"totalData", @"currentData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

//2、注册KVO观察
[self.person addObserver:self forKeyPath:@"currentProcess" options:(NSKeyValueObservingOptionNew) context:NULL];

//3、触发属性值变化
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.currentData += 10;
    self.person.totalData  += 1;
}

//4、移除观察者
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"currentProcess"];
}


KVO观察: 可变数组

在KVC官方文档中,针对可变数组的集合类型,有如下说明,即访问集合对象需要需要通过mutableArrayValueForKey方法,这样才能将元素添加到可变数组中

请添加图片描述

//1、注册可变数组KVO观察者
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
    [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
    
//2、KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}

//3、移除观察者
- (void)dealloc{
 [self.person removeObserver:self forKeyPath:@"dateArray"];
}

//4、触发数组添加数据
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // KVC 集合 array
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}


运行结果如下,可以看到,元素被添加到可变数组了

请添加图片描述

kind (表示键值变化的类型)

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,//设值
    NSKeyValueChangeInsertion = 2,//插入
    NSKeyValueChangeRemoval = 3,//移除
    NSKeyValueChangeReplacement = 4,//替换
};

一般的属性与集合的KVO观察是有区别的,其kind不同,以属性name 和 可变数组为例

  • 属性的kind一般是设值
  • 可变数组的kind一般是插入

请添加图片描述

KVO Crash 的常见原因

1.KVO 添加次数和移除次数不匹配:

移除了未注册的观察者,导致崩溃。 重复移除多次,移除次数多于添加次数,导致崩溃。
重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。

2. 被观察者提前被释放,被观察者在 dealloc 时仍然注册着 KVO,导致崩溃。 例如:被观察者是局部变量的情况(iOS 10及之前会崩溃)

3. 添加了观察者,但未实现 observeValueForKeyPath:ofObject:change:context: 方法,导致崩溃。

4.添加或者移除时 keypath == nil,导致崩溃。

KVO 防止 Crash 常见方案

为了避免上面提到的使用 KVO 造成崩溃的问题,于是出现了很多关于 KVO 的第三方库,比如最出名的就是 FaceBook 开源的第三方库 facebook / KVOController。

KVO 底层原理探索

请添加图片描述

KVO是使用isa-swizzling的技术实现的。

顾名思义,isa指针指向维护分配表的对象的类。该分派表实质上包含指向该类实现的方法的指针以及其他数据。

当为对象的属性注册观察者时,将修改观察对象的isa指针,指向中间类而不是真实类。结果,isa指针的值不一定反映实例的实际类。

您永远不应依靠isa指针来确定类成员身份。相反,您应该使用class方法来确定对象实例的类。

代码调试探索

KVO只对属性观察

KVO对成员变量不观察,只对属性观察,属性和成员变量的区别在于属性多一个 setter 方法,而KVO恰好观察的是setter 方法

中间类

在注册KVO观察者后,观察对象的isa指针指向会发生改变

注册观察者之前:实例对象person的isa指针指向LGPerson

请添加图片描述

注册观察者之后:实例对象person的isa指针指向NSKVONotifying_LGPerson

请添加图片描述

在注册观察者后,实例对象的isa指针指向由LGPerson类变为了NSKVONotifying_LGPerson中间类,即实例对象的isa指针指向发生了变化

判断中间类是否是派生类 即子类?

那么这个动态生成的中间类NSKVONotifying_LGPerson和LGPerson类 有什么关系?下面通过代码来验证

可以通过下面封装的方法,获取LGPerson的相关类

#pragma mark - 遍历类以及子类
- (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(@"classes = %@", mArray);
}

//********调用********
[self printClasses:[LGPerson class]];


请添加图片描述

从结果中可以说明NSKVONotifying_LGPerson是LGPerson的子类

中间类中有什么?(方法/属性)

可以通过下面的方法获取NSKVONotifying_LGPerson类中的所有方法

#pragma mark - 遍历方法-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);
}

//********调用********
[self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];


请添加图片描述

从结果中可以看出有四个方法,分别是setNickName 、 class 、 dealloc 、 _isKVOA,这些方法是继承还是重写?

  • 在LGStudent中重写setNickName方法,获取LGStudent类的所有方法
    请添加图片描述

与中间类的方法进行的对比说明只有重写的方法,才会在子类的方法列表中遍历打印出来,而继承的不会在子类遍历出来

请添加图片描述

  • NSKVONotifying_LGPerson中间类重写了父类LGPerson的setNickName方法
  • NSKVONotifying_LGPerson中间类重写了基类NSObject的class 、 dealloc 、 _isKVOA方法其中dealloc是释放方法
  • _isKVOA判断当前是否是kvo类
dealloc中移除观察者后,isa指向是谁,以及中间类是否会销毁?

移除观察者之前:实例对象的isa指向仍是NSKVONotifying_LGPerson中间类

请添加图片描述

移除观察者之后:实例对象的isa指向更改为LGPerson类

请添加图片描述

所以,在移除kvo观察者后,isa的指向由NSKVONotifying_LGPerson变成了LGPerson

那么中间类从创建后,到dealloc方法中移除观察者之后,是否还存在?

在上一级界面打印LGPerson的子类情况,用于判断中间类是否销毁

请添加图片描述

  1. 通过子类的打印结果可以看出,中间类一旦生成,没有移除,没有销毁

2.还在内存中 – 主要是考虑重用的想法,即中间类注册到内存中,为了考虑后续的重用问题,所以中间类一直存在

KVO 调用顺序

而我们前面说了,有一个中间类的存在,既然要生成中间类,肯定是有意义的,我们梳理一下整个 KVO 的流程,从注册观察者到观察者的回调通知,既然有回调通知,那么肯定是在某个地方发出回调的,而由于中间类是不能编译的,所以我们对中间类的父类也就是 JHPerson 类,我们重写一下相应的 setter 方法,我们不妨测试一下:

// JHPerson.m
- (void)setName:(NSString *)name
{
    _name = name;
}

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

- (void)didChangeValueForKey:(NSString *)key{
    NSLog(@"didChangeValueForKey - begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey - end");
}

请添加图片描述

也就是说 KVO 的调用顺序是:

  • 调用 willChangeValueForKey:
  • 调用原来的 setter 实现
  • 调用 didChangeValueForKey:

也就是说 didChangeValueForKey: 内部必然是调用了 observer 的observeValueForKeyPath:ofObject:change:context:方法。

总结
  • 实例对象isa的指向在注册KVO观察者之后,由原有类更改为指向中间类
  • 中间类重写了观察属性的setter方法、class、dealloc、_isKVOA方法
  • dealloc方法中,移除KVO观察者之后,实例对象isa指向由中间类更改为原有类
  • 中间类从创建后,就一直存在内存中,不会被销毁

自定义KVO

模仿系统自定义实现

实现思路

请添加图片描述

具体实现代码

导入头部信息
#import "NSObject+KVO.h"
#import <objc/runtime.h>
#import <objc/message.h>

static NSString *const kZMKVOPrefix = @"ZMKVONotifying_";
static NSString *const kZMKVOAssiociateKey = @"kZMKVO_AssiociateKey";
创建NSObject+KVO.h 分类

因为只要可以使用KVC的类都可以使用KVO所以通过分类提供添加观察者的方法

外部接口设计

#import <Foundation/Foundation.h>
#import "KVOInfo.h"


NS_ASSUME_NONNULL_BEGIN

@interface NSObject (KVO)

// 观察者注册
- (void)zm_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(ZMKeyValueObservingOptions)options context:(nullable void *)context;
// 回调通知观察者
- (void)zm_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
// 移除观察者
- (void)zm_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;

@end

了避免与系统的方法冲突,所以添加了一个方法前缀

自定义观察者注册

1.KVO 关注的是属性的 setter 方法

那其实判断对象所属的类是否有这样的 setter 就相当于同时判断了 keyPath 是否存在

2.需要去动态的创建子类,创建子类的过程中包括了重写 setter 等一系列方法

3. 就需要保存观察者和 keyPath 等信息

借助关联对象来实现,我们把传入的观察者对象、keyPath和观察策略封装成一个新的对象存储在关联对象中
同一个对象的属性可以被不同的观察者所观察,所以这里实质上是以对象数组的方式存储在关联对象里面

自定义的 JHKVOInfo 对象
// .h 文件
#import <Foundation/Foundation.h>

typedef NS_OPTIONS(NSUInteger, ZMKeyValueObservingOptions) {
    ZMKeyValueObservingOptionNew = 0x01,
    ZMKeyValueObservingOptionOld = 0x02,
};


NS_ASSUME_NONNULL_BEGIN

@interface KVOInfo : NSObject

@property (nonatomic, weak) NSObject  *observer;
@property (nonatomic, copy) NSString  *keyPath;
@property (nonatomic, assign) ZMKeyValueObservingOptions options;
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(ZMKeyValueObservingOptions)options;

@end

NS_ASSUME_NONNULL_END


// .m 文件
#import "KVOInfo.h"

@interface KVOInfo ()

@end

@implementation KVOInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(ZMKeyValueObservingOptions)options
{
    if (self = [super init]) {
        _observer = observer;
        _keyPath = keyPath;
        _options = options;
    }
    return self;
}

@end
zm_addObserver: 方法

- (void)zm_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(ZMKeyValueObservingOptions)options context:(void *)context
{
    // 1.验证是否存在setter 方法:不让实例进来
    if (![self checkSetterMethodFromKeyPath:keyPath]) {
        return;
    }
    
    // 2.动态创建中间子类 ZMKVONotifying_ZMPerson
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    
    // 3.将对象的isa指向为新的中间子类
    object_setClass(self, newClass);
    
   // 4. 抽象观察者
    KVOInfo *info = [[KVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
    
    // 5.保存观察者对象
    NSHashTable *map = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kZMKVOAssiociateKey));
    if (!map) {
        map = [NSHashTable hashTableWithOptions:NSPointerFunctionsStrongMemory];
        [map addObject:info];
        // 添加关联对象
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kZMKVOAssiociateKey), map, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
}
1.checkSetterMethodFromKeyPath 验证setter 方法是否存在
- (BOOL)checkSetterMethodFromKeyPath:(NSString *)keyPath {
    
    Class superCls = object_getClass(self);
    SEL setterSelector = NSSelectorFromString(setterForGetter(keyPath));
    
    Method setterMethod = class_getInstanceMethod(superCls, setterSelector);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:[NSString stringWithFormat:@"当前%@的没有 setter",keyPath] userInfo:nil];
        return NO;
    }
    return YES;
}

对象所属的类上是否有要观察的 keyPath 对应的 setter 方法

#pragma mark - 获取set方法的名称 (key ===>>> setKey:)
// 比如 name ===>>> setName:
static NSString * setterForGetter(NSString *keyPath)
{
   // 判断 keyPath 是否为空字符串
   if (keyPath.length <= 0) {
       return nil;
   }
   // 取出 getter 字符串的第一个字母并转大写
   NSString *firstLetter = [[keyPath substringToIndex:1] uppercaseString];
   // 取出剩下的字符串内容
   NSString *remainingLetters = [keyPath substringFromIndex:1];
   // 将首字母大写的字母与剩下的字母拼接起来得到 `set<KeyPath>` 格式的字符串
   NSString *setter = [NSString stringWithFormat:@"set%@%@:", firstLetter, remainingLetters];
   return setter;
}
2. 动态创建中间子类 ZMKVONotifying_ZMPerson

思路

  1. 中间子类的类名是:ZMKVONotifying_原始类名
    2.创建过不需要创建,用原来的,没有则创建
    3.重写zm_class方法/zm_setter方法/zm_dealloc方法
#pragma mark - 创建KVO中间子类
// 原始类 ZMPerson
// 中间类 ZMKVONotifying_ZMPerson

- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    // 1.获得原始类的类名
    NSString *oldClassName = NSStringFromClass([self class]);
    // 2.在原始类名前添加中间子类的前缀来获得中间子类名
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kZMKVOPrefix,oldClassName];
    // 3.通过中间子类名来判断是否创建过
    Class newClass = NSClassFromString(newClassName);
    // 4.如果创建过中间子类,直接返回
    if (newClass) return newClass;
    // 5.如果没有创建过,则需要创建一下,
       /*
        objc_allocateClassPair 方法的三个参数分别为:
        1.父类
        2.新类的名字
        3.创建新类所需额外的空间
        */
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    // 6.注册中间子类
    objc_registerClassPair(newClass);
    
    // 7. 中间子类上添加一个新的子类实现 `zm_class` == 达到对调用者隐藏中间子类的效果
    // 从父类上拿到 `class` 方法的 `SEL` 以及类型编码,然后在
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)zm_class, classTypes);
    
    // 8. 中间子类上添加一个新的子类实现 `zm_setter` == 通知所有观察者值的更改
    // 从父类上拿到 `setter` 方法的 `SEL` 以及类型编码,然后在
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)zm_setter, setterTypes);
    
    // 9. 添加 dealloc 方法 === isa 指回 ZMPerson
    SEL deallocSel = NSSelectorFromString(@"dealloc");
    // 拿到 MyPerson 的 setter 方法
    Method deallocMethod = class_getInstanceMethod([self class], deallocSel);
    // 得到方法签名
    const char *deallocType = method_getTypeEncoding(deallocMethod);
    // 添加方法
    class_addMethod(newClass, deallocSel, (IMP)zm_dealloc, deallocType);
    
    return newClass;
}

重写的方法

// 重写的class方法 === 达到对调用者隐藏中间子类的效果
Class zm_class(id self,SEL _cmd) {
   // 通过 class_getSuperclass 来返回父类的 `Class`,达到对调用者隐藏中间子类的效果
   return class_getSuperclass(object_getClass(self));
}
// 子类重写的setter imp
static void zm_setter(id self,SEL _cmd,id newValue){
    
    // 因为是重写父类的 `setter`,所以还需要通过消息发送的方式手动执行以下父类的 `setter` 方法
    // 通过强转的方式将 `objc_msgSendSuper` 转成 `zm_msgSendSuper` 函数指针,同时,由于 `objc_msgSendSuper` 要比我们常见的 `objc_msgSend` 多一个父类结构体参数,所以需要手动构建一下这个父类结构体,结构体有两个属性,分别是实例对象以及实例对象的类的父类
    void (*zm_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... */
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    // 准备工作完成后手动调用 `jh_msgSendSuper`,因为 `superStruct` 是结构体类型,而 `zm_msgSendSuper` 的第一个参数是空指针对象,所以这里需要加取地址符来把结构体地址赋值给指针对象
    
    // 1. 手动执行以下父类的 `setter` 方法
    zm_msgSendSuper(&superStruct, _cmd, newValue);
    
    
    // 因为 `_cmd` 作为方法的第二个参数其实就是 `setter` 的 `SEL`,这里反向获得对应 `getter` 字符串形式作为 `keyPath`,然后通过 `KVC` 来获取到旧的属性值
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue       = [self valueForKey:keyPath];
    // 调用完父类的 `setter` 之后,从关联对象中取出存储了自定义的对象数组
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kZMKVOAssiociateKey));
    // 循环遍历自定义的对象
    for (KVOInfo *info in observerArr) {
    // 如果 `keyPath` 匹配则进入下一步
        if ([info.keyPath isEqualToString:keyPath]) {
            // 基于线程安全的考虑,使用 `GCD` 的全局队列异步执行下面的操作
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                // 初始化一个通知字典
                NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
                // 判断存储的观察策略,如果是新值,则在通知字典中设置新值
                if (info.options & ZMKeyValueObservingOptionNew) {
                    [change setObject:newValue forKey:NSKeyValueChangeNewKey];
                }
                // 如果是旧值,在通知字典中设置旧值
                if (info.options & ZMKeyValueObservingOptionOld) {
                    [change setObject:@"" forKey:NSKeyValueChangeOldKey];
                    if (oldValue) {
                        [change setObject:oldValue forKey:NSKeyValueChangeOldKey];
                    }
                }
                // 取得通知观察者方法的 `SEL`
                SEL observerSEL = @selector(zm_observeValueForKeyPath:ofObject:change:context:);
                // 2.通过 `objc_msgSend` 手动发送消息,达到观察者收到回调的效果
                ((void(*)(id, SEL, id, id, NSMutableDictionary *, void *))objc_msgSend)(info.observer, observerSEL, keyPath, self, change, NULL);
            });
        }
    }
}

static void zm_dealloc(id self,SEL _cmd){
    
    NSLog(@"VC走了,自动销毁observer %s",__func__);
    // isa 指回 ZMPerson
    object_setClass(self, [self class]);
}

getterForSetter 实现如下:

// setName: ===>>> name
#pragma mark - 通过 getter 方法得到 setter 的名字 -

static NSString *getterForSetter(NSString *setter){
    // 判断传入的 `setter` 字符串长度是否大于 0,以及是否有 `set` 的前缀和 `:` 的后缀
    if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
    // 排除掉 `setter` 字符串中的 `set:` 部分以取得 getter 字符串
    NSRange range = NSMakeRange(3, setter.length-4);
    NSString *getter = [setter substringWithRange:range];
    // 对 getter 字符串首字母小写处理
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return  [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}
3. NSHashTable 保存观察者

{keypath : [KVOInfo 对象1, KVOInfo 对象2, … ]}

添加观察者时:通过关系哈希表判断是否重复添加,只添加一次。
移除观察者时:通过关系哈希表是否已经进行过移除操作,避免多次移除。

自定义移除观察者

思路

1.需要对关联对象中存储的自定义对象数组对应的观察者移除掉。
2.需要把 isa 指回原来的类,

// 移除观察者
- (void)zm_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context {
    
    // 1.从关联对象中取出数组
    NSHashTable *observerArr = objc_getAssociatedObject(self, (__bridge const void *)(kZMKVOAssiociateKey));
    // 如果数组中没有内容,说明没有添加过观察者,那么直接返回
    if (observerArr.count<=0) {
        return;
    }
    // 2. 则从数组中移除响应对象,然后存储最新的数组到关联对象上
    // 遍历取出的所有自定义对象
    for (KVOInfo *info in observerArr) {
        // 如果 `keyPath` 匹配上了 则从数组中移除响应对象,然后存储最新的数组到关联对象上
        if ([info.keyPath isEqualToString:keyPath]) {
            [observerArr removeObject:info];
            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kZMKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            break;
        }
    }
    
    // 3.要将 `isa` 指回原来的类的前提条件是,被观察属性的对象已经没有任何观察者在观察了,那么就需要指回去
    if (observerArr.count <=0 ) {
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
    
}
实现自动移除观察者

要执行 addObserver 和 removeObserver 的配套操作,难免有些繁琐。虽然一般来说为了方便起见,都是在观察者的
dealloc 方法中去手动调用 removeObserver 方法,但还是太麻烦了

思路

在子类中重写dealloc方法,
ZMPerson发送消息释放即dealloc了,就会自动走到重写的zm_dealloc方法中

上面代码已有

请添加图片描述

请添加图片描述

其原理主要是:

原因是因为person对象的isa指向变了,指向中间类,但是实例对象的地址是不变的,所以子类的释放,相当于释放了外界的person,而重写的zm_dealloc相当于是重写了ZMPerson的dealloc方法,所以会走到zm_dealloc方法中,达到自动移除观察者的目的

总结

自定义KVO大致分为以下几步

注册观察者 & 响应
1、验证是否存在setter方法

2、保存信息

3、动态生成子类,需要重写class、setter方法

4、在子类的setter方法中向父类发消息,即自定义消息发送

5、让观察者响应

移除观察者
1、更改isa指向为原有类

2、重写子类的dealloc方法

函数式编程思想KVO重构

  • 1、将注册和响应通过函数式编程,即block的方法结合在一起
  • 2.去掉系统繁琐的三部曲,实现KVO自动销毁机制

步骤:
1.传进来一个block
2.在zm_setter 方法由原来消息发送改为block回调
3.子类重写KVO zm_dealloc 实现自动销毁机制

KVOInfo 类的改变


// .h 文件
#import <Foundation/Foundation.h>

typedef void (^ZMKVOBlock)(NSObject *observer, NSString *keyPath, id oldValue, id newValue);

typedef NS_OPTIONS(NSUInteger, ZMKeyValueObservingOptions) {
    ZMKeyValueObservingOptionNew = 0x01,
    ZMKeyValueObservingOptionOld = 0x02,
};


NS_ASSUME_NONNULL_BEGIN

@interface KVOInfo : NSObject

@property (nonatomic, weak) NSObject  *observer;
@property (nonatomic, copy) NSString  *keyPath;
@property (nonatomic, assign) ZMKeyValueObservingOptions options;
@property (nonatomic, copy) ZMKVOBlock handleBlock;
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(ZMKeyValueObservingOptions)options handleBlock:(ZMKVOBlock)block;

@end

NS_ASSUME_NONNULL_END

// .m 文件

#import "KVOInfo.h"

@interface KVOInfo ()

@end

@implementation KVOInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(ZMKeyValueObservingOptions)options handleBlock:(ZMKVOBlock)block
{
    if (self = [super init]) {
        _observer = observer;
        _keyPath = keyPath;
        _options = options;
        _handleBlock = block;
    }
    return self;
}

@end

NSObject+KVO.h 分类的改变

NSObject+KVO.h 文件改动
NS_ASSUME_NONNULL_BEGIN

@interface NSObject (KVO)

// 观察者注册
- (void)zm_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(ZMKeyValueObservingOptions)options context:(void *)context handleBlock:(ZMKVOBlock)block;

@end

NS_ASSUME_NONNULL_END
zm_addObserver: 的改动
  // 4. 抽象观察者
    KVOInfo *info = [[KVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options handleBlock:block];
zm_setter 的改动

//                // 2.通过 `objc_msgSend` 手动发送消息,达到观察者收到回调的效果
//                ((void(*)(id, SEL, id, id, NSMutableDictionary *, void *))objc_msgSend)(info.observer, observerSEL, keyPath, self, change, NULL);
  // 由原来的消息发送改为block回调              
                if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
                    info.handleBlock(info.observer, keyPath, oldValue, newValue);
                }

KVOController

KVOController下载地址

KVO一种非常有用的技术,用于在模型-视图-控制器应用程序中的层之间进行通信,FBKVOController在系统的KVO的基础上做了一层封装,它提供了一个简单、现代的API,也是线程安全的。好处包括

  • 使用了block回调,自定义actions,原有的NSKeyValueObserving 回调
  • 实现了自动移除observer,无需开发则手动移除 线程安全
  • 使用block的形式,是代码更加集中。

KVOController流程图如下:

请添加图片描述

FBKVOController架构设计图

请添加图片描述

FBKVOController源码详解

FBKVOController源码详解分四部分
两个私有类

  • FBKVOInfo
  • FBKVOSharedController

两个公开类

  • FBKVOController
  • NSObject+FBKVOController

FBKVOController

首先我们创建一个FBKVOController的实例对象时,有以下三种方法,一个类方法和两个对象方法,

//该方法是一个全能初始化的对象方法,其他初始化方法内部均调用该方法
//参数:observer是观察者,retainObserved:表示是否强引用被观察的对象
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved 

//该初始化方法内部调用上一个初始化方法,默认强引用被观察的对象
- (instancetype)initWithObserver:(nullable id)observer;

//该初始化方法内部调用上一个初始化方法,默认强引用被观察的对象
+ (instancetype)controllerWithObserver:(nullable id)observer;
NS_DESIGNATED_INITIALIZER;
全能初始化方法讲解

全能初始化方法内部的实现,该方法对三个实例变量
_observer(观察者)
_objectInfosMap(NSMapTable,被监听对象->被监听属性集合之间的映射关系),
pthread_mutex_init(互斥锁):

//全能初始化方法
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if (nil != self) {
      
    //观察者
    _observer = observer;

//NSMapTable中的key可以为对象,而且可以对其中的key和value弱引用
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
      
//对于静态分配的互斥量, 可以把它设置为PTHREAD_MUTEX_INITIALIZER
//对于动态分配的互斥量, 在申请内存(malloc)之后, 通过pthread_mutex_init进行初始化, 并且在释放内存(free)前需要调用pthread_mutex_destroy
    pthread_mutex_init(&_lock, NULL);
  }
  return self;
}
这里请先思考以下问题:
  1. 属性observer为何使用weak,它和哪个对象之间会导致循环引用问题,是如何导致循环引用问题的?
  2. 为何不使用字典来保存被监听对象和被监听属性集合之间的关系?
  3. NSDictionary的局限性有哪些?NSMapTable相对字典,有哪些优点?
  4. 互斥锁是为了保证哪些数据的线程安全?

带着这些问题我们来看FBKVOController内部是如何实现监听的,这里我们只看带Block回调的一个监听方法,其他几个方法和这个方法内部实现是相同的。下面的方法内部做了如下工作:

1.传入的参数keyPath,block为空时,程序闪退,同时报出误提示;
2.对传入参数为空的判读;
3.利用传入的参数创建_FBKVOInfo对象;
4.调用内部私有方法实现注册监听;

//观察者监听object中健值路径(keyPath)所对应属性的变化
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
//NSAssert是一个预处理宏, 它可以让开发者比较便捷的捕获错误, 让程序闪退, 同时报出错误提示
  NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);

//首先判断被监听的对象是否为空,被监听的健值路径是否为空,回调的block是否为空
  if (nil == object || 0 == keyPath.length || NULL == block) {
    return;
  }

  // 根据传进来的参数创建_FBKVOInfo对象,将这些参数封装到_FBKVOInfo对象中
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

  // 监听对象object的属性信息(_FBKVOInfo对象)
  [self _observe:object info:info];
}

该私有方法内部并没有实现真正的注册监听,这里使用NSMapTable保存了被监听对象object-> _FBKVOInfo对象集合的关系,具体的监听是在_FBKVOSharedController类中实现的。观察者可以监听多个对象,而每个对象中可能有多个属性被监听,其关系如下图:

请添加图片描述

内部实现思路:

  • 对当前线程访问的数据_objectInfosMap进行加锁;
  • 根据被监听对象object到_objectInfosMap取出被监听的属性信息对象集合infos;
  • 判断被监听的属性对象info是否存在集合中;
  • 如果已经存在,则不需要再次添加监听,防止多次监听;
  • 如果获取的集合infos为空,则建存放_FBKVOInfo对象的集合infos,保存映射关系:object->infos;
  • 将被监听的信息_FBKVOInfo对象存到集合infos中;
  • 解锁,其他线程可以访问该数据;
  • 调用_FBKVOSharedController 的方法实现监听;
//该方法是内部私有方法
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  //先加锁,访问_objectInfosMap
  pthread_mutex_lock(&_lock);

    //到_objectInfosMap中根据key(被监听的对象)获取被监听的属性信息集合
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

   //判断infos集合中是否存在被监听属性信息对象info
  _FBKVOInfo *existingInfo = [infos member:info];

    //被监听对象的属性已经存在,不需要再次监听,防止多次添加监听
  if (nil != existingInfo) {
  
  //解锁,其他线程可以再次访问_objectInfosMap中的数据
    pthread_mutex_unlock(&_lock);
    return;
  }

  //根据被监听对象在_objectInfosMap获取的被监听属性信息的集合为空
  if (nil == infos) {
    //懒加载创建存放_FBKVOInfo对象的set集合infos
    infos = [NSMutableSet set];

    //保存被监听对象和被监听属性信息的映射关系object->infos
    [_objectInfosMap setObject:infos forKey:object];
  }

  // 将被监听的信息_FBKVOInfo对象存到集合infos中
  [infos addObject:info];

  //解锁
  pthread_mutex_unlock(&_lock);

   //最终的监听方法是通过_FBKVOSharedController中的方法来实现
  //_FBKVOSharedController内部实现系统KVO方法
  [[_FBKVOSharedController sharedController] observe:object info:info];
}

_FBKVOInfo

_FBKVOInfo私有类的内部很简单,没有任何业务逻辑,只是一个简单的Model,主要是将以下的实例变量封装到对象中,方便访问:

{
@public
//weak,防止循环引用
  __weak FBKVOController *_controller;
   //被监听属性的健值路径
  NSString *_keyPath;

//NSKeyValueObservingOptionNew:观察修改前的值
// NSKeyValueObservingOptionOld:观察修改后的值
//NSKeyValueObservingOptionInitial:观察最初的值(在注册观察服务时会调用一次触发方法)
//NSKeyValueObservingOptionPrior:分别在值修改前后触发方法(一次修改有两次触发)
  NSKeyValueObservingOptions _options;

//被监听属性值变化时的回调方法
  SEL _action;

//上下文信息(void * 任何类型)
  void *_context;
//被监听属性值变化时的回调block
  FBKVONotificationBlock _block;
//监听状态
  _FBKVOInfoState _state;
}

_FBKVOInfo私有类提供了一个全能初始化方法,来初始化以上实例变量。其他几个部分初始化方法内部均调用该全能初始化方法。

//全能初始化方法
- (instancetype)initWithController:(FBKVOController *)controller
                           keyPath:(NSString *)keyPath
                           options:(NSKeyValueObservingOptions)options
                             block:(nullable FBKVONotificationBlock)block
                            action:(nullable SEL)action
                           context:(nullable void *)context
{
  self = [super init];
  if (nil != self) {
    _controller = controller;
    _block = [block copy];
    _keyPath = [keyPath copy];
    _options = options;
    _action = action;
    _context = context;
  }
  return self;
}
优化判断对象相等性的效率:

同时_FBKVOInfo私有类还重写了isEqual:和hash方法,用来进行_FBKVOInfo对象的判等性。当我们在自定义对象时,需要重写isEqual:和hash方法,作为自定义对象相等性的判断。

优化判断对象相等性的效率:
1.首先判断hash值是否相等,若相等则进行第2步;若不等,则直接判断不等;hash值是对象判等的必要非充分条件;(即没它一定不行,有它不一定行)
2.在hash值相等的情况下,再进行对象判等, 作为判等的结果;
关于对象相等性判断

//当重写hash方法时,我们可以将关键属性的hash值进行位或运算来作为hash值
- (NSUInteger)hash
{
  return [_keyPath hash];
}

/**
 对于基本类型, ==运算符比较的是值;
 对于对象类型, ==运算符比较的是对象的地址(即是否为同一对象)
 */
- (BOOL)isEqual:(id)object
{
    //判断对象是否为空,若为空,则不相等
  if (nil == object) {
    return NO;
  }

    //判断对象的地址是否相等,若相等,则为同一个对象(即是否为同一个对象)
  if (self == object) {
    return YES;
  }
    
    //判断是否是同一类型,这样可以提高判等的效率, 还可以避免隐式类型转换带来的潜在风险
  if (![object isKindOfClass:[self class]]) {
    return NO;
  }
    
    //对各个属性分别使用默认判等方法进行判断
    //返回所有属性判等的与结果
  return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath];
}

请分析如果将实例变量__weak FBKVOController *_controller前的 __weak去掉,它和_FBKVOInfo对象之间的循环引用环是如何形成的?

_FBKVOSharedController

_FBKVOSharedController私有类内部实现了系统KVO的方法,用来接收和转发KVO的通知。接口中提供了监听和移除监听的方法。其接口如下:

@interface _FBKVOSharedController : NSObject

// 单例初始化方法
+ (instancetype)sharedController;

// 监听object的属性
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info;

//移除对object中属性的监听
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info;

// 移除对object中多个属性的监听
- (void)unobserve:(id)object infos:(nullable NSSet *)infos;

@end

_FBKVOSharedController私有类内部有两个私有成员变量,

  • _infos是用来存放_FBKVOInfo对象,

_infos可以对其中的成员变量弱引用,这也是为何使用NSHashTable,而不使用NSSet来存放_FBKVOInfo对象的原因。

  • _mutex是互斥锁:
{
    //存放被监听属性的信息对象
  NSHashTable<_FBKVOInfo *> *_infos;
    //互斥锁
  pthread_mutex_t _mutex;
}

_FBKVOSharedController私有类的初始化方法,支持iOS 系统和Mac系统,初始化实例变量_infos,指定了_infos对存放在其中的成员变量弱引用,及判等性方式:

//提供全局的单例初始化方法,该单例对象的生命周期与程序的生命周期相同
+ (instancetype)sharedController
{
  static _FBKVOSharedController *_controller = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    _controller = [[_FBKVOSharedController alloc] init];
  });
  return _controller;
}
//初始化成员变量_infos和_mutex
- (instancetype)init
{
  self = [super init];
  if (nil != self) {
    //初始化实例变量
    NSHashTable *infos = [NSHashTable alloc];
      
   // iOS 系统下:hashTable中的对象是弱引用,对象的判等方式:位移指针的hash值和直接判等
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
    _infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];

  //MAC系统下
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
    if ([NSHashTable respondsToSelector:@selector(weakObjectsHashTable)]) {
      _infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
    } else {
      // silence deprecated warnings
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
      _infos = [infos initWithOptions:NSPointerFunctionsZeroingWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#pragma clang diagnostic pop
    }

#endif
      //初始化互斥锁
    pthread_mutex_init(&_mutex, NULL);
  }
  return self;
}

- (void)dealloc
{
    //对象被销毁时,销毁互斥锁
  pthread_mutex_destroy(&_mutex);
}

_FBKVOSharedController在这个方法中,调用系统KVO方法,将自己注册为观察者,思路如下:

1.首先将被监听的信息对象_FBKVOInfo保存到_infos中;
2.然后调用系统KVO方法将自己注册为被监听对象object的观察者;
3.最后修改监听的状态;当不再监听时,安全移除观察者;

//添加监听
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
    //被监听的属性信息_FBKVOInfo对象为空时,直接返回
  if (nil == info) {
    return;
  }

    // 加锁,防止多线程访问时,出现数据竞争
  pthread_mutex_lock(&_mutex);

   // 将被监听的属性信息info对象添加到_infos中,_infos对成员变量info是弱引用
  [_infos addObject:info];   
 
    //添加完成之后,解锁,其他线程可以访问
  pthread_mutex_unlock(&_mutex);
  
  // 添加监听
  [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];

  //修改监听状态
  if (info->_state == _FBKVOInfoStateInitial) {
      
    info->_state = _FBKVOInfoStateObserving;
      
  } else if (info->_state == _FBKVOInfoStateNotObserving) {
      
      //不再监听时安全移除观察者
    // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
    // and the observer is unregistered within the callback block.
    // at this time the object has been registered as an observer (in Foundation KVO),
    // so we can safely unobserve it.
    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  }
}

实现系统KVO监听回调的方法

//被监听属性更改时的回调
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSString *, id> *)change
                       context:(nullable void *)context
{
  NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);

  _FBKVOInfo *info;
  {
    pthread_mutex_lock(&_mutex);
  //确定_infos是否包含给定的对象context,若存在返回该对象,否则返回nil;
  //所使用的相等性比较取决于所选择的选项
  //例如,使用NSPointerFunctionsObjectPersonality选项将使用isEqual:方法来判断相等。
    info = [_infos member:(__bridge id)context];
    pthread_mutex_unlock(&_mutex);
  }

//通过上下文参数context传过来的被监听的_FBKVOInfo对象,已经存在_infos中
  if (nil != info) {
  
 //_FBKVOSharedController对象强引用FBKVOController对象,防止被提前释放
 //因为在_FBKVOInfo中,对FBKVOController对象是弱引用
    FBKVOController *controller = info->_controller;
    if (nil != controller) {

      //强引用观察者,在FBKVOController中,FBKVOController对象弱引用观察者observer,防止在使用时已经被释放
      id observer = controller.observer;
      if (nil != observer) {

        //使用自定义block回传监听结果
        if (info->_block) {

          NSDictionary<NSString *, id> *changeWithKeyPath = change;

          //将keyPath添加到字典中以便在观察多个keyPath时,能够清晰知道监听的是哪个keyPath
          if (keyPath) {
            NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
            [mChange addEntriesFromDictionary:change];
            changeWithKeyPath = [mChange copy];
          }
          info->_block(observer, object, changeWithKeyPath);

        } else if (info->_action) {
//使用自定义方法回传监听结果
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
          [observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
        } else {
          //使用系统默认方法回传监听结果
          [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
        }
      }
    }
  }
}

_FBKVOSharedController实现了移除观察者的方法,思路如下:

1.首先从_infos中移除被监听的属性信息对象info;
2.然后根据监听状态,通过调用系统的方法,移除正在被监听的属性信息对象info;
3.最后修改监听状态;

- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info
{
  if (nil == info) {
    return;
  }

    //先从HashTable中移除被监听的属性信息对象
  pthread_mutex_lock(&_mutex);
  [_infos removeObject:info];
  pthread_mutex_unlock(&_mutex);

  // 当正在监听时,则移除监听
  if (info->_state == _FBKVOInfoStateObserving) {
    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  }
    //修改被监听的状态
  info->_state = _FBKVOInfoStateNotObserving;
}

NSObject+FBKVOController

NSObject+FBKVOController 分类比较简单,它主要通过runtime方法,以懒加载的形式给 NSObject ,创建并关联一个 FBKVOController 的对象。

@interface NSObject (FBKVOController)
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
@end

FBKVOController总结

FBKVOController是线程安全的,相对于系统的KVO而言,使用起来更方便,安全,简洁。
1.NSHashTable和NSMapTable的使用;
2.互斥锁pthread_mutex_t的使用
3.FBKVOController和Observer之间循环引用的形成和解决;
4.FBKVOController和_FBKVOInfo之间循环引用的形成和解决;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值