KVO使用以及原理分析

基础使用

使用KVO需要三个步骤:

  1. 在观察者中,调用被观察者的addObserver:forKeyPath:options:context:进行注册
  2. 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法
  3. 在观察者中使用removeObserver:forKeyPath:移除KVO,一般可以在dealloc方法中移除,否则会导致Crash

1 注册观察者

比方说ViewController是观察者,person是其属性。ViewController需要监听person中的age属性

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:nil];
[self.person addObserver:self forKeyPath:@"numArray" options:options context:nil];

options是一个NS_OPTIONS枚举,可以决定以下内容

  • 通知发出的时机
  • observeValueForKeyPath:ofObject:change:context:方法中,字典参数change字典中包含哪些值

参数options有四个值

  1. NSKeyValueObservingOptionNew,change字典中应该包含改变后的新值

  2. NSKeyValueObservingOptionOld,change字典中应该包含改变前的旧值

  3. NSKeyValueObservingOptionInitialaddObserver:forKeyPath:options:context:消息被发出去后,甚至不用等待这个消息返回,监听者对象会马上收到一个通知。这种通知只会发送一次,你可以利用这种“一次性“的通知来确定要监听属性的初始值。当同时制定这3个选项时,这种通知的change字典中只会包含新值,而不会包含旧值。虽然这时候的新值实际上是改变前的'旧值',但是这个值对于监听者来说是新的。

  4. NSKeyValueObservingOptionPrior:当指定了这个选项时,在被监听的属性被改变前,监听者对象就会收到一个通知(一般的通知发出时机都是在属性改变后,虽然change字典中包含了新值和旧值,但是通知还是在属性改变后才发出),这个通知会包含一个NSKeyValueChangeNotificationIsPriorKeykey,其对应的值为一个NSNumber类型的YES。当同时指定该值、new和old的话,change字典会包含旧值而不会包含新值。

2 接收通知

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<nskeyvaluechangekey, id> *)change context:(nullable void *)context;</nskeyvaluechangekey, id>

关于change参数,它是一个字典,有五个常量作为它的键:

  1. NSKeyValueChangeKindKey:指明了变更的类型,值为NSKeyValueChange枚举中的某一个,类型为NSNumber。
    • 一般情况下返回的都是第一个NSKeyValueChangeSetting
    • 如果监听的属性是一个集合对象的话,当这个集合中的元素被插入,删除,替换时,就会分别返回NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement
enum {
 NSKeyValueChangeSetting = 1,
 NSKeyValueChangeInsertion = 2,
 NSKeyValueChangeRemoval = 3,
 NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;
  1. NSKeyValueChangeNewKey:被监听属性改变后新值的key。当监听属性为一个集合对象,且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时,该值返回的是一个数组,包含插入,替换后的新值(删除操作不会返回新值)。

  2. NSKeyValueChangeOldKey:被监听属性改变前旧值的key。当监听属性为一个集合对象,且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时,该值返回的是一个数组,包含删除,替换前的旧值(插入操作不会返回旧值)

  3. NSKeyValueChangeIndexesKey:如果NSKeyValueChangeKindKey的值为NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement,这个键的值是一个NSIndexSet对象,包含了增加,移除或者替换对象的index

  4. NSKeyValueChangeNotificationIsPriorKey:如果注册监听者是options中指明了NSKeyValueObservingOptionPrior,change字典中就会带有这个key,值为NSNumber类型的YES.

3. 移除监听

当一个监听者完成了它的监听任务之后,就需要注销(移除)监听者,调用以下2个方法来移除监听。通常会在-dealloc方法或者observeValueForKeyPath:ofObject:change:context:方法中移除。

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context
或者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

有几点需要注意的:

当你向一个不是监听者的对象发送remove消息的时候(也可能是,你发送remove消息时,接受消息的对象已经被remove了一次,或者在注册为监听者前就调用了remove),xcode会抛出一个NSRangeException异常,所以,保险的做法是,把remove操作放在try/catch中。

一个监听者在其被销毁时,并不会自己注销监听,而给一个已经销毁的监听者发送通知,会造成野指针错误。所以至少保证,在监听者被释放前,将其监听注销。保证有一个add方法,就有一个remove方法。

4 触发KVO

触发KVO的方式包括:

  • 点语法
  • set方法
  • kvc
  • 如果监听的是集合属性,以数组为例,得使用被观察者mutableArrayValueForKey,进行操作
[[self.person mutableArrayValueForKey:@"numArray"] addObject:@(randNum)];

KVO原理

当某个对象第一次被观察时,系统就会在运行期动态地创建该类的一个子类类对象(类名就是在该类的前面加上NSKVONotifying_ 前缀),被观察对象的isa指针就指向这个类对象。这个子类类对象,会有如下的变化:

  • 被观察属性的set方法被修改
  • 修改class实例方法,让其返回原始的类对象
  • dealloc_isKVOA
    KVO之后类的关系.png

自己实现KVO的步骤

  1. NSObject的分类,提供addObserver的接口
  2. 判断isa是否指向KVONotify_class,否则生成该类
    • 使用objc_allocateClassPair生成子类
    • 给子类添加KVONotify_class.class方法,返回原始的类
    • 使用object_setClass(self, kvoClass),让被观测对象isa指向KVONotify_class
  3. 给KVONotify_class添加setter方法实现,观察属性的setter都会走这个实现
    • 从setter selector解析出属性,调用objc_msgSend方法,发送getter消息,获取oldValue
    • 使用msgSendSuper,调用原始class上的setter方法
    • 取出被观察对象的关联对象,是一个数组,元素是一个自定义类,包含观测者,观测属性,block等
  4. 向被观察对象添加关联对象,是一个数组,元素是一个自定义类,包含观测者,观测属性,block等

参考

//www.greatytc.com/p/badf5cac0130
Demo地址:https://github.com/xiaoLong1010/DeepObjectiveC/tree/master/03-KVO

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

推荐阅读更多精彩内容

  • 0917 六排最佳 墨辰君 0918 六排最佳 墨辰君/小小丽 故事主题营今日优秀 墨辰君 0919 ...
    剽悍文霞阅读 157评论 0 0
  • 读大学时,我是插班进来的,心里可能会觉得我跟他们不一样,本以为我可以融进这个集体啦,可是,我拼命想融入的却又融入不...
    橙子在阳光下阅读 356评论 2 2
  • 晕死了,家里什么宠物都没养,没想到我和他都被跳蚤咬了。可能和最近天气一直潮湿中带着蒙蒙细雨有关,而住处后面还有...
    清汤寡水bh阅读 188评论 0 0
  • 春天是最有生命力的季节,也是宝宝成长最快的时候。让宝宝健康的度过换季,日常饮食十分重要! 妈妈最愁的是宝宝不肯吃饭...
    淘菜猫优选阅读 191评论 0 0