KVO 基础知识

KVO, 英文全称 Key-Value observing, 中文全称键值调查。KVO是一种机制,当指定的目标特点被修改后,则目标就会接受到告诉(简略的说就是每次指定的被调查的目标特点被修改后,KVO 就会主动告诉相应的调查者)。

经过 Key-Value Observing Programming Guide 官方文档,咱们了解到

1. KVO 是建立在 KVC 基础之上

  • KVC 是键值编码,创立目标后,能够经过 KVC 动态的给目标特点赋值。
  • KVO 是键值调查,供给一种监听机制。当指定的目标特点被修改后,则目标会收到告诉。
  • 因而能够看出 KVO 是建立在 KVC 的基础上对特点动态改动的监听。

2. KVO 根本运用(注册、完结回调、移除)三部曲

– 注册调查者

[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
  • 参数observer为调查者:调查目标经过上面办法发送音讯向调查目标进行注册,并将本身作为调查者传递。

  • 参数keyPath为特点途径:在注册时调查者将调查的特点途径专递给调查目标。

  • 参数options为调查选项:影响告诉中供给的字典的内容以及生成告诉的办法。枚举类型:NSKeyValueObservingOptions

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
  NSKeyValueObservingOptionNew,   // 使告诉中发送的字典中包括 NSKeyValueChangeNewKey 键,用于包括特点产生改动后的新值
  NSKeyValueObservingOptionOld,   // 使告诉中发送的字典中包括NSKeyValueObservingOptionOld键,用于包括特点产生改动前的旧值
  NSKeyValueObservingOptionInitial, // 会在注册的时分使调查办法调用一次
  NSKeyValueObservingOptionPrior  // 会在调查特点值改动的前后均调用一次。其间在改动前调用那一次,会使 NSKeyValueObservingOptionNew 对应的值为 NULL,而 NSKeyValueObservingOptionOld 对应改动前的值。多用在开发者手动创立支持的 KVO 中
};

经过官方文档对 context 的描绘,大致意思是:addObserver:forKeyPath:options:context:办法中的上下文context指针包括任意数据,这些数据将在相应的更改告诉中传递回调查者。能够经过指定context为NULL,然后经过keyPath键途径字符串来确认更改告诉的来源,可是这种办法可能会导致目标的父类由于不同的原因也调查到相同的键途径而导致问题。因而可认为每个调查到的keyPath创立一个不同的context,然后完全不需求进行字符串比较,就能够更有效地进行告诉解析。
浅显的讲,context上下文主要是用于区分不同目标的同名特点,然后在 KVO 回调办法中能够直接运用context进行区分,能够大大提升性能,以及代码的可读性

*不运用 context 时, 则直接传 NULL,而不是 nil。因为 context 的类型是 nullable void

[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 回调

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"%@",change);
    }
}

– 移除 KVO

[self.person removeObserver:self forKeyPath:@"name" context:NULL];

3. 移除 KVO 的重要性 经过官方文档对 KVO 的移除重要性也有阐明,大致意思是:

  • 要求被移除为调查者(假如没有注册为调查者)会导致NSRangeException。您能够对removeObserver:forKeyPath:context:进行一次调用,以对应对addObserver:forKeyPath:options:context:的调用,或许假如在您的应用中不可行,则将removeObserver:forKeyPath:context:调用在try / catch块内处理潜在的反常。

  • 开释后,调查者不会主动将其本身移除。被调查目标持续发送告诉,而忽略了调查者的状态。可是,与发送到已开释目标的任何其他音讯相同,更改告诉会触发内存拜访反常。因而,您能够保证调查者在从内存中消失之前将自己删去

  • 该协议无法问询目标是调查者仍是被调查者。构造代码以避免发布相关的过错。一种典型的方式是在调查者初始化期间(例如,在init或viewDidLoad中)注册为调查者,并在开释过程中(通常在dealloc中)注销,以保证成对和有序地增加和删去音讯,并确保调查者在注册之前被撤销注册,从内存中开释出来

所以总的来说,KVO 注册调查者 和移除调查者是需求成对呈现的,假如只注册不移除,会呈现相似野指针的溃散
举个比方:由于第一次注册 KVO 调查者后没有移除,再次进入界面,会第二次注册 KVO 调查者,导致KVO 调查的重复注册。并且第一次的告诉目标还在内存中,没有进行开释,此刻接收到特点值改动的告诉,会呈现找不到原有的告诉目标,只能找到现有的告诉目标,即第二次KVO注册的调查者,所以导致了相似野指针的溃散,即一向保持着一个野告诉,且一向在监听。

注:这儿的溃散事例是经过单例目标完结(溃散有很大的几率,不是每次必现),因为单例目标在内存是常驻的,针对一般的类目标,形似不移除也是能够的。

4. KVO 的主动触发和手动触发

  • 主动开关:重写这个办法,回来 NO,则不监听。回来 YES, 则监听。默许不重写则回来 YES。
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return YES;
}
  • 主动开关设置为 NO 时,也能够经过手动敞开监听。运用手动开关的优点就是你想监听就监听,不想监听关闭即可,比主动触发更便利灵敏。
- (void)setName:(NSString *)name{
    //手动开关
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

5. KVO 调查一对多

KVO调查中的一对多,意思是经过注册一个KVO调查者,能够监听多个特点的改动
以下载进展为例,比方目前有一个需求,需求根据总的下载量totalData当时下载量currentData 来计算当时的下载进展currentProcess,完结有两种办法:

  • 办法一: 别离调查 总的下载量totalData 和当时下载量currentData 两个特点,当其间一个产生改动,计算当时下载进展currentProcess
  • 办法二: 完结keyPathsForValuesAffectingValueForKey办法,将两个调查合为一个调查,即调查当时下载进展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"];
}

6. KVO 调查可变数组

KVO 是建立在 KVC 基础之上,所以可变数组假如直接增加数据是不会调用setter办法的。一切对可变数组的 KVO 调查下面这种办法不收效的,即直接经过[self.person.dateArray addObject:@"1"]向数组增加元素,是不会触发 KVO 告诉回调的。所以有必要经过[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"]这种办法。

iOS 深化了解 KVO 实质
从图中咱们能够看到kind表示键值改动的类型,是一个枚举。主要有以下4种:

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

一般的特点调集KVO调查是有区别的,其kind不同,以特点name可变数组为例

  • 特点kind一般是设值
  • 可变数组kind一般是刺进

KVO 底层原理(重点)

经过官方文档对 KVO 的描绘,大致意思是:

  • KVO是运用isa-swizzling的技能完结的。
  • 望文生义,isa指针指向保护分配表的目标的类。该分配表实质上包括指向该类完结的办法的指针以及其他数据。
  • 当为目标的特点注册调查者时,将修改调查目标的isa指针指向中心类而不是真实类。成果,isa指针的值不一定反映实例的实践类。
  • 您永远不该依托isa指针来确认类成员身份。相反,您应该运用class办法来确认目标实例的类

1. KVO 只对特点调查

@interface LGPerson : NSObject{
  @public
  NSString *userName;
}
@property (nonatomic, copy) NSString *nickName;
  • 别离为成员变量userName和特点nickName注册KVO调查。
    self.person = [[LGPerson alloc] init];
    [self.person addObserver:self forKeyPath:@"userName" options:(NSKeyValueObservingOptionNew) context:NULL];
    self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
  • KVO告诉触发操作。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"实践情况:%@-%@",self.person.nickName,self.person->userName);
    self.person->userName = @"Justin";
    self.person.nickName = @"黑之契约者";
}

运转成果如图:

iOS 深化了解 KVO 实质
定论:经过代码测验发现 KVO 只对特点调查,成员变量不调查。特点和成员变量的区别在于特点多一个 setter 办法,而KVO恰好调查的是 setter 办法

2. 中心类

经过官方文档描绘,在注册KVO调查者后,调查目标的isa指针指向会产生改动

  • 注册调查者之前:实例目标personisa指针指向LGPerson

iOS 深化了解 KVO 实质

  • 注册调查者之后:实例目标personisa指针指向NSKVONotifying_LGPerson

iOS 深化了解 KVO 实质
定论:经过代码调试发现在注册调查者后,实例目标的 isa 指针指向由LGPerson类变成了NSKVONotifying_LGPerson中心类,即实例目标的isa指针指向产生了改动。
2.1 问题1:动态生成的中心类 NSKVONotifying_LGPersonLGPerson 类有什么关系? 能够经过下面封装的办法来验证,此办法能够遍历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);
}

运转成果如图:

iOS 深化了解 KVO 实质
定论:别离在增加 KVO 前和增加 KVO 后调用printClasses办法,从成果中能够阐明NSKVONotifying_LGPersonLGPerson的子类。

2.2 问题2:动态生成的中心类内部有哪些东西?
能够经过下面封装的办法来验证,此办法能够获取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);
}

运转成果如图:

iOS 深化了解 KVO 实质
从运转成果中咱们看到有四个办法,别离是setNickName、class、dealloc、_isKVOA。这些办法是承继仍是重写
答:重写。能够新建一个LGStudent类承继LGPerson类,重写setNickName。然后调用

[self printClassAllMethod:[LGStudent Class]];

运转成果如图:

iOS 深化了解 KVO 实质

运转成果阐明只要重写才会在子类的办法列表中遍历打印出来,而承继的不会在子类遍历出来。
因而定论:

  • NSKVONotifying_LGPerson中心类重写父类LGPersonsetNickName办法
  • NSKVONotifying_LGPerson中心类重写基类NSObjectclass 、 dealloc 、 _isKVOA办法
  • 其间dealloc是开释办法
  • _isKVOA判别当时是否是 KVO 类

2.3 问题3:dealloc 中移除调查者后,isa指向是谁,以及中心类是否会毁掉?

  • 移除调查者之前如图:

iOS 深化了解 KVO 实质

  • 移除调查者之后如图:

iOS 深化了解 KVO 实质
定论:移除调查者之前,实例目标的 isa 指向仍是NSKVONotifying_LGPerson中心类。移除调查者之后:实例目标的 isa 指向更改为LGPerson

那么中心类从创立后,到 dealloc 办法中移除调查者之后,是否还存在?

运转成果:

iOS 深化了解 KVO 实质
定论:当咱们回来到上级之后,经过调用printClasses打印发现动态生成的子类仍是存在的。由此能够确认中心类一旦生成,没有移除,没有毁掉,还在内存中。主要是考虑重用的主意,即中心类注册到内存中,为了考虑后续的重用问题,所以中心类一向存在
总结

  • 实例目标isa的指向在注册 KVO 调查者之后,由原有类更改为指向中心类(NSKVONotifying_原有类名)
  • 中心类重写了调查特点的setter办法classdealloc_isKVOA办法。
  • dealloc 办法中,移除 KVO 调查者之后,实例目标isa指向由中心类更改为原有类
  • 中心类从创立后,就一向存在内存中,不会被毁掉

自界说 KVO

自界说 KVO 仅仅帮助咱们更好的理解 KVO 底层完结原理,个人并不主张在项目中去界说 KVO。想在项目中安全便捷的运用 KVO 的话,推荐运用 facebook 的开源第三方结构 KVOController有兴趣的小伙伴能够自行下载源码学习。
体系 KVO 完结相对繁琐,注册调查者和 KVO 呼应归于呼应式编程,是分隔写的。而咱们自界说为了代码更好的协调,运用block的方式,将注册和回调的逻辑组合在一起,即采用函数式编程办法,仍是分为三部分:

  • 注册调查者
  1. 判别当时调查值 keyPath 的setter办法是否存在。
#pragma mark - 验证是否存在setter办法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath
{
    Class superClass = object_getClass(self);
    SEL setterSelector = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(superClass, setterSelector);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"CJLKVO - 没有当时%@的setter办法", keyPath] userInfo:nil];
    }
}
  • 2、动态生成子类,将需求重写的class办法增加到中心类中。
#pragma mark - 动态生成子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath
{
    //获取原本的类名
    NSString *oldClassName = NSStringFromClass([self class]);
    //拼接新的类名
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kMPKVOPrefix,oldClassName];
    //获取新类
    Class newClass = NSClassFromString(newClassName);
    //假如子类存在,则直接回来
    if (newClass) return newClass;
    //2.1 申请类
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    //2.2 注册
    objc_registerClassPair(newClass);
    //2.3 增加 class 办法
    SEL classSel = @selector(class);
    Method classMethod = class_getInstanceMethod([self class], classSel);
    const char *classType = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSel, (IMP)mp_class, classType);
    return newClass;
}
//*********class办法*********
#pragma mark - 重写class办法,为了与体系类对外保持一致
Class mp_class(id self, SEL _cmd){
    //在外界调用class回来CJLPerson类
    return class_getSuperclass(object_getClass(self));//经过[self class]获取会造成死循环
}
  1. isa 指向中心类。
object_setClass(self, newClass);
  1. 保存信息:这儿用的数组,也能够运用 map,需求创立信息的model模型类。
//*********KVO信息的模型类/*********
#pragma mark 信息model类
@interface MPKVOInfo : NSObject
@property(nonatomic, weak) NSObject *observer;
@property(nonatomic, copy) NSString *keyPath;
@property(nonatomic, copy) LGKVOBlock handleBlock;
- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block;
@end
@implementation MPKVOInfo
- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block{
    if (self = [super init]) {
        _observer = observer;
        _keyPath = keyPath;
        _handleBlock = block;
    }
    return self;  
}
@end
//*********保存信息*********
//- 保存多个信息
MPKVOInfo *info = [[MPKVOInfo alloc] initWithObserver:observer forKeyPath:keyPath handleBlock:block];
//运用数组存储 -- 也能够运用map
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
if (!mArray) {//假如mArray不存在,则从头创立
    mArray = [NSMutableArray arrayWithCapacity:1];
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];

完好的注册调查者代码如下:

#pragma mark - 注册调查者 - 函数式编程
- (void)mp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(MPKVOBlock)block{
    //1、验证是否存在setter办法
    [self judgeSetterMethodFromKeyPath:keyPath];
    //保存信息
    //- 保存多个信息
    MPKVOInfo *info = [[MPKVOInfo alloc] initWithObserver:observer forKeyPath:keyPath handleBlock:block];
    //运用数组存储 -- 也能够运用map
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kMPKVOAssociateKey));
    if (!mArray) {//假如mArray不存在,则从头创立
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kMPKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
    //2、动态生成子类、
    /*
        2.1 申请类
        2.2 注册
        2.3 增加办法
     */
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    //3、isa指向
    object_setClass(self, newClass);
    //获取sel
    SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
    //获取setter实例办法
    Method method = class_getInstanceMethod([self class], setterSel);
    //办法签名
    const char *type = method_getTypeEncoding(method);
    //增加一个setter办法
    class_addMethod(newClass, setterSel, (IMP)mp_setter, type); 
}

注:class办法有必要重写,其意图是为了与体系相同,对外的类保持一致。
体系的KVO,在增加调查者前后,实例目标person的类一向都是CJLPerson
假如没有重写class办法,自定的KVO在注册前后的实例目标person的 class 就会看到是不一致的,回来的isa 更改后的类,即中心类。
重写后 class 办法后的自界说 KVO,在注册调查者前后其实例目标类的显现,与体系的显现是一致的。

  • KVO 呼应 主要是给子类动态增加setter办法,其意图是为了在setter办法中向父类发送音讯,经过block的办法传递给外部进行呼应。 5、将setter办法重写增加到子类中(主要是在注册调查者办法中增加)。
//获取sel
    SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
    //获取setter实例办法
    Method method = class_getInstanceMethod([self class], setterSel);
    //办法签名
    const char *type = method_getTypeEncoding(method);
    //增加一个setter办法
    class_addMethod(newClass, setterSel, (IMP)mp_setter, type);

6、经过将体系的objc_msgSendSuper强制类型转化自界说的音讯发送cjl_msgSendSuper

//往父类LGPerson发音讯 - 经过objc_msgSendSuper
//经过体系强制类型转化自界说objc_msgSendSuper
void (*mp_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
//界说一个结构体
struct objc_super superStruct = {
    .receiver = self, //音讯接收者 为 当时的self
    // super_class = [self class]
    .super_class = class_getSuperclass(object_getClass(self)), //第一次方便查找的类 为 父类
};
//调用自界说的发送音讯函数
mp_msgSendSuper(&superStruct, _cmd, newValue);

7、奉告vc去呼应:获取信息,经过block传递。

/*---函数式编程*/
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kMPKVOAssociateKey));
    for (MPKVOInfo *info in mArray) {
        NSMutableDictionary<NSKeyValueChangeKey, id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
           info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }

完结的 setter 办法代码如下:

static void mp_setter(id self, SEL _cmd, id newValue) {
    NSLog(@"来了:%@",newValue);
    //此刻应该有willChange的代码
    //往父类LGPerson发音讯 - 经过objc_msgSendSuper
    //经过体系强制类型转化自界说objc_msgSendSuper
    void (*mp_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
    //界说一个结构体
    struct objc_super superStruct = {
        .receiver = self, //音讯接收者 为 当时的self
        .super_class = class_getSuperclass(object_getClass(self)), //第一次方便查找的类 为 父类
    };
    //调用自界说的发送音讯函数
    mp_msgSendSuper(&superStruct, _cmd, newValue);
    //让vc去呼应
    /*---函数式编程*/
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kMPKVOAssociateKey));
    for (MPKVOInfo *info in mArray) {
        NSMutableDictionary<NSKeyValueChangeKey, id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
           info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }
}
  • 移除调查者 避免在外界不断的调用removeObserver办法,在自界说 KVO 中完结主动移除调查者。 8.完结mp_removeObserver:forKeyPath:办法,主要是清空数组,以及isa指向更改。
- (void)mp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    //清空数组
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
    if (mArray.count <= 0) {
        return;
    }
    for (MPKVOInfo *info in mArray) {
        if ([info.keyPath isEqualToString:keyPath]) {
            [mArray removeObject:info];
            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kMPKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
    }
    if (mArray.count <= 0) {
        //isa指回父类
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

9、在子类中重写dealloc办法,当子类毁掉时,会主动调用dealloc办法(在动态生成子类的办法中增加)。

#pragma mark - 动态生成子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
    //...
    //增加dealloc 办法
    SEL deallocSel = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSel);
    const char *deallocType = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSel, (IMP)mp_dealloc, deallocType);
    return newClass;
}
//************重写dealloc办法*************
void mp_dealloc(id self, SEL _cmd) {
    NSLog(@"来了");
    Class superClass = [self class];
    object_setClass(self, superClass);
}

其原理主要是:MPPerson发送音讯开释即dealloc了,就会主动走到重写的mp_dealloc办法中(原因是因为person目标的isa指向变了,指向中心类,可是实例目标的地址是不变的,所以子类的开释,适当于开释了外界的person,而重写的mp_dealloc适当于是重写了MPPerson的dealloc办法,所以会走到mp_dealloc办法中),达到主动移除调查者的意图。

总结

自界说 KVO 大致分为以下几步:

  • 注册调查者 & 呼应
    1.验证是否存在setter办法。
    2.保存信息。
    3.动态生成子类,需求重写classsetter办法。
    4.在子类的setter办法中向父类发音讯,即自界说音讯发送。
    5.让调查者呼应。
  • 移除调查者
    1.更改isa指向为原有类。
    2.重写子类的dealloc办法。