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自定义实现

完整代码只是基本实现

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,695评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,569评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,130评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,648评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,655评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,268评论 1 309
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,835评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,740评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,286评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,375评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,505评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,185评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,873评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,357评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,466评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,921评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,515评论 2 359

推荐阅读更多精彩内容