KVO

342 阅读5分钟

前言

KVO的简要介绍

KVO的释放以及崩溃情况

KVO官方文档

[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];

正常情况下,我们可以通过KVO的方式去观察一个对象的属性发生的变化,(context传NULL是因为他是一个void *类型,而不是一个简单的对象类型,对象类型是传nil)、

我们在一般写KVO的时候,并不会手动去添加dealloc方法去移除观察者,因为苹果在ios9以后,对这个没有要求,也就是说随着ViewController的消失,观察者也会自动回收,但是也有特殊情况,比如:

     self.student = [LGStudent shareInstance];

    //  weak observer

    [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];

我们把对象写成一个单例,保证他在ViewController消失的时候不会被回收。这时候再次进去,依然会对VC进行观察,但是此时VC已经不存在了,原先的指针变成了野指针,便会产生崩溃,所以,为了规范和保险起见,还是有必要在当前类做好观察者的移除工作!

手动开关

// 自动开关

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}

当开关关闭时,是收不到观察消息的,必须手动打开设置。

- (void)setNick:(NSString *)nick{

    [self willChangeValueForKey:@"nick"];

    _nick = nick;

    [self didChangeValueForKey:@"nick"];

}

当需要观察一个对象的多个属性

keyPathsForValuesAffectingValueForKey方法,然后setByAddingObjectsFromArray放到这个数组里即可,相当于把影响因素放到数组中处理。

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{


    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];

    if ([key isEqualToString:@"downloadProgress"]) {

        NSArray *affectingKeys = @[@"totalData", @"writtenData"];

        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];

    }

    return keyPaths;

}

对可变数组的观察

KVO对数组的观察是基于KVC的,所以直接add的方式是无法添加观察的。

截屏2021-07-28 下午4.53.18.png

截屏2021-07-28 下午4.53.36.png

通过mutableArrayValueForKey可以添加观察,但是kind为什么是2呢?看一下定义?

截屏2021-07-28 下午4.55.41.png

试一下remove?

截屏2021-07-28 下午4.54.39.png

KVO的探索原理

截屏2021-07-28 下午5.01.01.png

这段话翻译:

自动键值观测是使用isa swizzling技术实现的。

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

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

决不能依赖isa指针来确定类成员身份。相反,应该使用class方法来确定对象实例的类.

简单地理解就是,在KVO的过程中会产生一个中间产物,可能是"中间类".

KVO的底层原理:

  1. 对象的isa并不会指向对象所在的类,而是会指向这个类所派生的类,也就是他的子类

  2. 观察的是子类的setter方法

通过打印KVO前后对象的isa指向的类可以看到,在这之后指向了新的类,那么这个类NSKVONotifying_NxPerson是什么呢?与原来的类是什么关系呢?

截屏2021-07-28 下午5.24.53.png

截屏2021-07-28 下午5.28.06.png

下列方法可以打印一个类的所有子类。

- (**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);

}

通过打印KVO前后,可以看到他的子类里新增了NSKVONotifying_Class这样的一个子类。

1627464854192.jpg

关于NSKVONotifying_Class

打印类的所有方法

- (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);

}

打印出四个方法(如果只观察一个属性的话) 截屏2021-07-28 下午5.43.55.png

_isKVOA:是否KVO标识,dealloc:释放,负责在dealloc的时候指回去class类方法以及set方法,并且这个set是重写了父类的方法。因为它能在自己的方法列表里找到,说明就是它自己的,不存在继承,继承的话还是在父类里。

关于派生类的dealloc处理

isa指向

在设置观察的时候,isa指向会发生变化,那么在dealloc的时候,指向会不会改变呢?

指回来了

截屏2021-07-28 下午6.04.22.png

派生类会不会销毁?

通过在pop出来的view上重新打印这个类的子类,可以看到仍然打出来了,说明这个派生类并不会销毁。

在派生类产生的时候,对象就指向了这个派生类,而不是原来的类,在dealloc的时候,改变了对象的指向,然后对象销毁。

关于KVO的set方法

set方法无法对成员变量监听,set方法可以很明显的区分是成员变量还是属性。那么这个set方法是LGPerson的还是NSKVO的?

当dealloc的时候,打印属性还是能发现值被修改了。

截屏2021-07-28 下午6.24.18.png

整体流程梳理

首先设置了观察者以后,isa指向了这个派生的子类,负责观察他的属性,一旦发生了改变就会调用set(NSKVO的)方法,具体就是willchange和didchange的方法也就是前面的那个手动修改的方法,但是这些都是底层自己去实现的,在表面上还是LGPerson,通过回调通知改变。在dealloc的时候指向回归到LGPerson,总结:派生子类就是工具人。木叶的三代和团藏罢了。

未完待续~~

自定义KVO