KVO底层分析

KVO概念

KVO ->Key-Value observing,键值观察,当被观察对象中指定属性发现变化时,观察者就可以得到通知,进而进行后续操作。

KVO使用

根据KVO官方文档 得知,正常使用大体分为以下流程:

  • 注册观察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
  • KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"%@",change[NSKeyValueChangeNewKey]);
    }
}
  • 移除观察者
[self.person removeObserver:self forKeyPath:@"nick" context:NULL];

context 用法及意义

截屏2022-01-04 下午5.54.09.png

根据官方文档解读可以得知:
context上下文主要作用就是防止根据keypath查询通知来源时,因父类子类观察到相同路径而出现的问题,不同的keypath创建不同的context,这样就可以不用通过字符串比较的方式去确定keypath,从而提高性能以及代码可读性

//定义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);
    }
}

观察者移除

截屏2022-01-04 下午6.04.25.png

根据官方文档可以得知:

  • 如果没有注册而移除了观察者,时就会出现NSRangeException,如果进行了一次addObserver,则对应的也需要removeObserver

  • 释放后,观察者不会自动将其自身移除。被观察对象继续发送通知,而忽略了观察者的状态。但是,与发送到已释放对象的任何其他消息一样,更改通知会触发内存访问异常。因此,您可以确保观察者在从内存中消失之前将自己删除。

  • 该协议无法询问对象是观察者还是被观察者。构造代码以避免发布相关的错误。一种典型的模式是在观察者初始化期间(例如,在init或viewDidLoad中)注册为观察者,并在释放过程中(通常在dealloc中)注销,以确保成对和有序地添加和删除消息,并确保观察者在注册之前被取消注册,从内存中释放出来

总结:KVO注册观察者(addObserver)与移除观察者(removeObserver)是成对出现的,如果只注册,不移除,则会出现野指针类型的崩溃,如果只移除,不注册,则会NSRangeException

KVO 的自动与手动触发

系统默认的事自动触发,即如果添加了观察者,并且回调方法中存在相应的处理,这时只要属性值发生改变,就会调用,如果关闭自动触发automaticallyNotifiesObserversForKey设置为NO,这时就需要将需要观察的属性改变前增加willChangeValueForKey,改变后增加didChangeValueForKey,这样就可以触发,通过手动触发能够更好地贴合项目中的需求,增加扩展性

KVO一对多

通过keyPathsForValuesAffectingValueForKey方法将多个属性合并成一个进行观察,以下载进度为例

+ (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];
self.person.writtenData += 10;
self.person.totalData  += 1;
[self.person removeObserver:self forKeyPath:@"downloadProgress"];
- (NSString *)downloadProgress{
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}

KVO 键值变化类型

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,//设值
    NSKeyValueChangeInsertion = 2,//插入
    NSKeyValueChangeRemoval = 3,//移除
    NSKeyValueChangeReplacement = 4,//替换
};
//根据下面代码来实现不同的kind 
self.student.name = [NSString stringWithFormat:@"%@+",self.student.name];
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
[[self.person mutableArrayValueForKey:@"dateArray"] removeLastObject];
[[self.person mutableArrayValueForKey:@"dateArray"] replaceObjectAtIndex:0 withObject:@"3"];
  • 注:当当前观察对象为数组时,不可以直接通过赋值的方式进行更改,而是需要mutableArrayValueForKeymutableArrayValueForKeyPath进行获取,然后才能实现更改回调

KVO 底层探究

由于KVO没有对应的开源代码,故而通过跟流程的方式查看

  • 观察的是setter方法
    验证:根据属性成员变量的区别可以得知,两者之间属性存在setter方法,而成员变量没有
@interface LGPerson : NSObject{
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;

@end
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"实际情况:%@-%@",self.person.nickName,self.person->name);
    self.person.nickName = @"KC";
     self.person->name    = @"Cooci";
}

#pragma mark - KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}

截屏2022-01-05 上午10.21.23.png

根据上述结果可以得知,属性nickName更改成功收到回调,而成员变量name没有收到回调,故而可以验证KVO观察的是setter方法

  • 中间类
    根据官方文档得知,在注册观察者之后,观察对象的isa会发生改变
    截屏2022-01-05 上午10.32.13.png

    根据断点获取className可以看出isa的指向确实发生了改变
 [self printClasses:[LGPerson class]];
    self.person = [[LGPerson alloc] init];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
//     [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self printClasses:[LGPerson class]];
    [self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];
    [self printClassAllMethod:[LGStudent class]];

#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);
}

#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);
}

通过遍历方法和子类可以看出


截屏2022-01-05 上午10.30.19.png
  • 在没有注册观察值之前,只存在LGPersonLGStudent两个类,而注册完成后,多出了一个NSKVONotifying_LGPerson,故而可以确定,NSKVONotifying_LGPersonLGPerson的子类
  • 通过观察NSKVONotifying_LGPerson中的方法列表可以看出,子类中存在setter方法,在LGStudent中实现了setNickName的重写,与NSKVONotifying_LGPerson中一致,故而可以确定,在NSKVONotifying_LGPerson实现了setter、class、dealloc、_isKVO重写,而非继承
截屏2022-01-05 下午1.23.08.png
  • 根据上图可以得知,在移除观察者之前isa指向的是子类NSKVONotifying_LGPerson,移除之后指回LGPerson
  • 虽然观察者移除了,但是在其它页面查看LGPerson子类时可以发现中间类NSKVONotifying_LGPerson没有销毁,这样可以避免每次进行创建而造成性能低下,通过重用的方式使得中间类一经创建就一直存在

KVO自定义实现

完整代码只是基本实现

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容