KVO的原理解析

331 阅读4分钟

在一些场景中我们有时候会用到KVO,KVO的原理是什么样子的呢,如果自己定义一个KVO要怎么做呢?

KVO

Key-value observing provides a mechanism that allows objects to be notified of changes to specific properties of other objects. KVO提供了一种机制,允许在其他对象的特定属性发生变化时通知对象。

通常我们使用KVO时需要以下三个步骤:

  1. 使用addObserver:forKeyPath:options:context:向被观察对象注册观察者。
  2. 实现observeValueForKeyPath:ofObject:change:context:这个方法来接受变更通知消息。
  3. 当观察者不再接受消息时,使用removeObserver:forKeyPath:方法注销观察者。至少,在从内存中释放观察者之前调用这个方法。
context

addObserver:forKeyPath:options:context:中context 截屏2021-07-29 下午9.30.44.png 在有多个对象的属性同时被监听时,我们可以申明不同的context来识别 eg:

截屏2021-07-29 下午9.31.47.png 截屏2021-07-29 下午9.32.23.png

关联变化keyPathsForValuesAffectingValueForKey

之前有做过一个图片管理器,里面显示的上传进度,但是在图片上传的时候,也会有别的图片在这个时刻被选择,然后加入到总的上传图片的队列里面,我们这个时候可以用如下方法监听 downloadProgress

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];

    }
    return keyPaths;
}

[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];这个时候你改变了totalDatawrittenDatadownloadProgress都可以被监听到。

可变数组添加变量的时候监听

eg1:

self.person.dateArray = [NSMutableArray arrayWithCapacity:1];

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

当我们使用[self.person.dateArray addObject:@"1"];我们是无法监听到dateArray 的变化的,要使用[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];

removeObserver:forKeyPath:一定要手动移除吗?

我们知道iOS9之后,对于普通的添加观察者的方法不需要手动移除self。 下面我们看一种情况: 在detailViewController中,有一个student的单例。我们给它添加观察者,这个页面消失的时候,由于student仍有之前的观察者,当多次进到详情页面,点击屏幕的时候,都会对观察者发送改变,而有的这个时候已经被内存回收了,就会出现EXC_BAD_ACCESS异常。

截屏2021-07-29 下午10.11.05.png

手动开关

在某些情况下,你可能希望控制通知流程。

截屏2021-07-29 下午10.23.34.png 对于我们想要通知的属性我们可以使用automaticallyNotifiesObserversForKey返回NO。 要实现手动观察者通知,可以调用willChangeValueForKeydidChangeValueForKey这两个方法 截屏2021-07-29 下午10.26.59.png

KVO的实现原理

主要实例代码:

//LGPerson.h
@interface LGPerson : NSObject{

    @public

    NSString *name;

}

@property (nonatomic, copy) NSString *nickName;

@end

//Person.m
@implementation Person


- (void)setNickName:(NSString *)nickName{

    _nickName = nickName;

}

//Student.h

#import "Person.h"
@interface Student : Person

@end

//Student.m
#import "Student.h"

@implementation Student

- (void)setNickName:(NSString *)nickName{   

}


@end

//ViewController.m
self.person = [[Person alloc] init];

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

截屏2021-07-29 下午10.29.43.png 由上面知道,观察者是使用isa-swizzling技术实现的。当观察者为一个对象的属性注册时,被观察对象的isa指针被修改,指向了一个中间类而不是真正的类。因此,isa指针的值不一定能反映实例的实际类。

  • ❓中间类里面做了什么呢
  • ❓中间类与原类有什么关系呢
  • ❓中间类是什么时候被销毁的呢 带着这些疑问我们来探究KVO的原理。
动态子类

截屏2021-07-30 上午10.03.03.png 我们在[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];前后打印self.person发现在添加完观察者之后,self.person指向了一个NSKVONotifying_LGPerson中间类。

与原类的关系

我们知道,在添加了观察者之后,元类的isa指针指向了一个NSKVONotifying_LGPerson这个类, 我们在添加观察者前后打印类的子类信息,打印方法如下

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

}

打印信息如下 截屏2021-07-30 上午10.06.03.png NSKVONotifying_PersonPerson的子类。

重写方法

中间类NSKVONotifying_Person做了什么呢,我们打印一下NSKVONotifying_Person的方法列表 遍历方法列表的方法实现如下:

- (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-30 上午10.10.14.png 打印入上图所示,在NSKVONotifying_Person中重写了

  • setNickName
  • class
  • dealloc
  • _isKVOA 这四个方法。
移除观察者的时候做了什么

截屏2021-07-30 上午10.12.49.png 从打印的数据看出,在移除观察的时候,isa指针又重新指回了过来。 NSKVONotifying_Person有销毁吗? 我们在前一个页面,打印一下Person的类信息,如下图

截屏2021-07-30 上午10.17.18.png 说明中间类没有销毁。

在添加了观察者之后,系统会自动生成一个动态子类,原类的isa指针会指向动态子类,动态子类与原类是父子关系,在动态子类中重写了setter、class、dealloc、_isKVOA这四个方法,在移除观察者的时候,isa的指针会重新指回来。