20-KVO分析

前言

什么是KVO(Key-Value Observing)

Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.
键值观察是一种机制,它允许对象在其他对象的指定属性发生更改时收到通知。

KVO官方地址

KVO基础

KVO 从日常的开发中看出无非就是三个api

  • (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
  • (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
  • (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

那么接下来就具体看看这几个API到底有何作用。

1、NSKeyValueObservingOptions 的作用。

NSKeyValueObservingOptionOldNSKeyValueObservingOptionNew 是我们常用的两个选选项。
下面通过一个 demo 来验证这个到底有什么作用
先准备如下一份代码

@interface CDPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, strong) NSMutableArray *dateArray;
@property (nonatomic, copy) NSArray *array;
@end

///实现如下一份代码
- (void)viewDidLoad {
    [super viewDidLoad]; 
    self.person = [CDPerson alloc];
    self.person.nick = @"Hello"; 
    [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionOld) context:NULL];
  ///  [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
  ///  [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionPrior) context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"change = %@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.nick = [self.person.nick stringByAppendingString:@"+"];
}

这时候我们分别监听几个不同的 options ,可以得到如下的结果

  1. NSKeyValueObservingOptionOld
change = {
    kind = 1;
    old = Hello;
}
  1. NSKeyValueObservingOptionNew
change = {
    kind = 1;
    new = "Hello+";
}
  1. NSKeyValueObservingOptionPrior
change = {
    kind = 1;
    notificationIsPrior = 1;
}
change = {
    kind = 1;
}

2、 context

上下文。这种设计在很多场景都有实用,特别是在CFCG等框架的时候。而从官方文档上来看就是 :

一种更安全、更可扩展的方法是使用上下文来确保您收到的通知是发送给您的观察者而不是超类的。
那么我们来验证一下

static void * personName = @"personName";
/// 2、验证 context
    [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:personName];
    

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

打印结果如下:

2021-07-29 23:02:04.270060+0800 001---KVO初探[10373:973646] change = {
    kind = 1;
    new = "Hello+";
}

2021-07-29 23:02:04.270130+0800 001---KVO初探[10373:973646] personName
2021-07-29 23:02:04.270192+0800 001---KVO初探[10373:973646] change = {
    kind = 1;
    new = "niubi-";
}

通过结果我们发现,这个context 确实可以被带到通知里面去。这样我们就可以更加好判断谁监听的谁。也可以保证在移除观察者的时候不会出现问题(不会把父类相同的监听给移除了)。

// 这样,即使父类也有一个观察了name 的观察者,只要context 不一样,就不会随意的移除掉。
[self.person removeObserver:self forKeyPath:@"name" context:personName]

3、要不要移除观察者

通常来说,我们注册的观察者一旦执行了 dealloc 以后,那么被观察的对象也就释放了。所以移除与否都没有关系。但是有一些情况是,虽然我的观察者释放了,但是这个被观察的对象依然还存在,那这个时候在给这个观察者发生通知那就会出问题了。比如我们上面的被观察的对象是个单列,或者其他一些暂时没办法释放的东西,那么下次在给当前对象发生通知就会触发野指针而崩溃。
所以,最好还是在我们观察者 dealloc 的时候,执行 remove

4、手动和自动监听KVO

api 里面还有一个 +automaticallyNotifiesObserversForKey:方法,这个方法默认返回 true。也就是默认开启自动发送通知,如果我们返回 false 那么久没发自动发送通知,需要手动发送通知,即调用 willChangeValueForKey:and didChangeValueForKey: 者两个方法来手动发出通知。也可以通过 + (BOOL)automaticallyNotifiesObserversOfName 这个方法来指定某个属性是和否可以自动发出通知(这个要在automaticallyNotifiesObserversForKey:没有重写的情况下)。

// 自动开关关闭
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return false;
}

当我们重写了如上的方法后,整个类的KVO 就不会自动触发通知的发送。这个时候就需要手动去触发:

- (void)setNick:(NSString *)nick{
    [self willChangeValueForKey:@"nick"];
    _nick = nick;
    [self didChangeValueForKey:@"nick"];
}

5、监听集合类型

如果我们要监听集合类型的属性(如:NSArray),那么我们实现如下监听。

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

如果直接改变数组的成员是不会触发的,只有按照KVC 的方式去触发才可以触发通知的发送。

/// 这样是不会生效的
[self.person.dateArray addObject:@"222"];

/// 需要下面这样
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"222"];
[[self.person mutableArrayValueForKey:@"array"] addObject:@"333"];
// 亦或者 
    [[self.person mutableArrayValueForKey:@"dateArray"] removeObject:@"2"];
    [[self.person mutableArrayValueForKey:@"array"] removeObject:@"3"];

当然这样执行集合类型的观察在配合 options 可以看看是什么效果,阁下可以自己去尝试看看结果是如何的。笔者这里就不在细说,还有包括KVC 的相关的一些对应的情况,可以查阅笔者关于KVC 的表述

6、监听keyPath 多级路径

self.person.st = [LGStudent alloc];
    self.person.st.name = @"student";
[self.person addObserver:self forKeyPath:@"st.name" options:(NSKeyValueObservingOptionNew) context:NULL]

//执行如下方法
self.person.st.name = [self.person.st.name stringByAppendingString:@"+"];

///打印结果如下:
change = {
    kind = 1;
    new = "student+";
}
change = {
    kind = 1;
    new = "student++";
}

KVO 实现

KVO 到底是如何实现的,接下来我们就去探索。这里借助LLDBapi 来一起验证。

1、探索isa

Automatic key-value observing is implemented using a technique called isa-swizzling.
从官方文档来看,自动KVO是一种 isa-swizzling,那么我们就先来看看这个isa 到底是什么,如下实现一段代码,并且下一个断点,分别在添加观察者和添加后打印结果

查看isa

从结果我们可以看出,在添加了观察者后,isa指向了一个 名为 NSKVONotifying_LGPerson 的类。那么这个类和我们的 LGPerson 有什么关系呢?那么结合我们前面类的原理里面探索的,类结构的第二个成员变量是 superClass ,可以得出他们是父子关系。

(lldb) po 0x00000001c28f8628
NSObject

(lldb) po 0x0000000104a55650
LGPerson

7、NSKVONotifying_CDPerson 里面有什么东西<成员变量、方法、协议>

这里笔者采用api来看看当前这个类里面到底有什么。
接下来调用如下一个方法来探索这个类里面有什么成员。

- (void)getAllMethodFromCls:(Class)cls {
    
    unsigned int count;
    Method *ms = class_copyMethodList(cls, &count);
    NSLog(@"**************** 方法: %@ : %d ****************", cls, count);
    for (int i = 0; i < count; i++) {
        SEL sel = method_getName(ms[I]);
        NSLog(@"SEL = %@", NSStringFromSelector(sel));
    }
     
    Ivar *ivs = class_copyIvarList(cls, &count);
    NSLog(@"**************** 成员变量: %@ : %d", cls, count);
    for (int i = 0; i < count; i++) {
        const char *cName = ivar_getName(ivs[I]);
        NSLog(@"Name = %@", [NSString stringWithCString:cName encoding:NSUTF8StringEncoding]);
    }
    
    objc_property_t *ps = class_copyPropertyList(cls, &count);
    NSLog(@"**************** 属性: %@ : %d", cls, count);
    for (int i = 0; i < count; i++) {
        const char *cName = property_getName(ps[I]);
        NSLog(@"Name = %@", [NSString stringWithCString:cName encoding:NSUTF8StringEncoding]);
    }
    NSLog(@"\n\n");
     
}

然后在监听前后监听后分别查看这个类的相关信息

[self getAllMethodFromCls:object_getClass(self.person)];
   
    [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self.person addObserver:self forKeyPath:@"st.name" options:(NSKeyValueObservingOptionNew) context:NULL];
    
    [self getAllMethodFromCls:object_getClass(self.person)];
    

这里笔者有个问题是设个 st.name 到底是在何处监听的?

观察前的结果
观察后的结果

从结果我们可以看到,并没有setsSt.name 这样的方法。只有一个 setSt:的方法,这就让我怀疑是不是 LGStudent 也有创建了一个动态了的类,而这种多级监听最后只是通过kvc 传递到了里面相关的对象里面去了。
通过调试我发现确实是这样的,LGStudent 耶动态生成了一个 NSKVONotifying_LGStudent 子类。

(lldb) po object_getClass(self.person.st)
NSKVONotifying_LGStudent

结论

经过前面这么多分析,KVO 的大致流程和原理我们野梳理的差不多了。

1、动态注册子类 NSKVONotifying_XXX。
2、判断当前是否是属性(因为需要重写setter: 方法)。
3、修改当前对象isa指针指向动态子类NSKVONotifying_XXX。
4、调用setter 方法,并且转发给父类同时发出通知通知观察者observeValueForKeyPath: ofObject: change: context:。
5、在调用removeObserver:forKeyPath: 后有将isa 指回原来的类。

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

推荐阅读更多精彩内容

  • 初探 Key-value observing is a mechanism that allows objects...
    xxxxxxxx_123阅读 639评论 2 2
  • 一、什么是KVO KVO和Notification是Objective-C语言中观察者模式的两种实现机制。KVO指...
    坤坤同学阅读 269评论 0 0
  • [TOC] (一)KVO 初探 1. 基本用法 添加观察 监听观察 移除观察 通知使用完之后,一定要移除,否则会有...
    修_远阅读 241评论 0 5
  • KVC KVC定义 KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过K...
    暮年古稀ZC阅读 2,117评论 2 9
  • KVC定义 KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过Key名直...
    SheIsMySin_72e7阅读 373评论 0 0