关于KVO的那些事 之 KVO安全用法封装

关于KVO的那些事 之 KVO安全用法封装

KVO (Key Value Observering) 是iOS用于监听某个对象某个变量一种简洁便利的机制。但是,对于KVO的稳定性苹果却做得没有那么好,在以下三种情况下会无情Crash:

  1. 监听者dealloc时,监听关系还存在。当监听值发生变化时,会给监听者的野指针发送消息,报野指针Crash。(猜测底层是保存了unsafe_unretained指向监听者的指针);
  2. 被监听者dealloc时,监听关系还存在。在监听者内存free掉后,直接会报监听者还存在监听关系而Crash;
  3. 移除监听次数大于添加监听次数。报出多次移除的错误;

我们考虑到KVO带来的伤害,平时十分小心翼翼在工程内使用KVO,甚至能不用的时候就不用。但我们不甘心将如此好用的机制沦为带刺玫瑰-----为KVO打造"紫金铠甲"。

根据崩溃类型,我们的目标也有三个:

  1. 监听者dealloc时,自动移除监听者对其他对象的监听,No Crash;
  2. 被监听者dealloc时,自动移除对被监听者所有的监听,No Crash;
  3. 移除监听次数大于添加监听次数时,多次监听/移除,只执行一次,No Crash;

实现的源码请点这里

1. 解决监听者(listener)dealloc的Crash

解决监听者dealloc的Crash,最直接的办法就是在监听者(listener)的dealloc调用[listened removeObserver:listener forKeyPath:path]。但此时我们会遇到3个问题:

  1. 如若不采用hook dealloc方法添加移除逻辑(采用hook风险比较大,会对所有NSObject对象的方法hook),还有什么方法能对所有对象的dealloc插上一脚呢?
  2. listener(监听者)不知道listened(被监听者)们是谁?
  3. listener不知道listened的keypath有哪些?

第一个问题的解决方案是 关联对象。当一个对象释放时,会进行以下三个步骤:第一步,销毁对象的所有属性及实例变量,第二步,移除对象上的所有关联对象;第三步,移除所有对该对象的weak引用。

KVO Crash三类

关联对象的释放是在listener dealloc过程的第二个步骤当中,此时对象并没有完全释放。因此,我们可以给listener添加一个关联对象解决第一个问题。

根据第一个问题解决方案,第二和第三个解决方案也不难想出。同样地,我们可用关联对象保存监听者和keypath数组实现。但我们不想这么做,为了让 listener 看起来更加干净,也为了让逻辑更加清晰,可将监听者和keypath作为第一个关联对象的实例变量。而且为了不强引用监听者,监听者是weak保存的。另外,为了全权管理listener对listened的行为,我们将监听行为转给这个关联对象,关联对象收到监听消息再转递给listener。至此,我们称这个关联对象为代理者(proxy)。

最终,listener,listened,proxy三者关系及监听者移除监听过程如下:

解listener Crash

proxy定义如下:

proxy 定义

我们来详细捋一遍过程:

添加过程

  1. 添加监听时,取出关联在listener上对象proxy(没有就创建,并建立新的监听关系和转发关系),且proxy以weak的方式保存了listener和listened两个对象;
  2. 如果keypath在proxy保存中,说明已经监听过了,不需要再监听。此时解决了我们第三大类的移除次数大于监听次数的crash。如不在keypath中,则添加keypath到keypath容器中,并建立对于新的keypath的监听关系;

dealloc过程

listener dealloc,触发proxy的dealloc,proxy根据保存的keypath信息依次移除监听关系,至此监听关系完美解除。

第一个和第三个Crash问题解决了,接下来,我们来fix第二大问题~

2. 解决被监听者(listened)dealloc的Crash

和第一个问题类似,要解决listened的dealloc的Crash,同样地可以给listened添加一个关联对象用于检测listened的释放时机。如下图所示:

图中的D对象,也就是监听被监听者的监听者(有点绕...),Listened's Dealloc Listener简称LDL。可以看到它不仅跟listened有关系,还弱持有了proxy。为什么呢?因为对于listened来说,它自己并不知道谁监听了它,而正好proxy知道监听中的所有秘密。当listened释放时,LDL被释放,根据保存的proxy关系,就能释放对listened的所有监听关系,并且还可以移除listener的关联对象proxy。

最终,我们来看看LDL的实现代码:

眼尖的同学会发现两个有趣的地方:

  1. listened 是 __unsafe_unretained的指针保存;
  2. proxy 是 用 weak NSHashTable 容器保存;

为什么要用__unsafe_unretained保存listened,而不是weak

原因是若使用weak,在LDL dealloc 的过程中,指针获取到的值已经为nil了(在proxy保存的listened也一样),拿不到我们要用的对象指针,那么好奇的宝宝又会问了:这又是为什么呢?这是因为每次使用weak变量时,最终会调用id objc_loadWeakRetained(id *)方法,方法发现当前对象如果在dealloc过程中就会直接返回nil。所以,我们这里使用了__unsafe_unretained指针来保有对象的指针,既能一直访问到对象,又不会影响对象的引用计数。

第二个问题,NSHashTable 容器保存proxy,NSHashTable类似于数组,但它可以保存任何指针,而且可以有各种方法存储他们,比如,retain, weak, copy...。而我们这里使用的目的是保存监听关系proxy列表,而且弱引用他们。这种特殊容器平时开发用的很少,想了解更多关于这些特殊容器的,请点这里

用法

原理讲完了,说说最终的用法,和系统源码API一样简单,提供了三个接口如下:

总结

本文从KVO三种类型的Crash进行分析,使用了代理模式做转发,用关联对象监听dealloc时机,使用__unsafe_unretained来持有不会增加引用计数但一直保有对象的指针,使用了不常见的NSHashTable来弱持有代理,最终实现了健壮的KVO,减少了KVO系统实现的问题导致的不愉快的使用体验,让更多人感受到使用KVO机制带来的幸福感。

源码请点我~

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

推荐阅读更多精彩内容

  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 3,012评论 0 26
  • 写在前面 程序设计语言中有各种各样的设计模式(pattern)和与此对应的反设计模式(anti-pattern),...
    Frankxp阅读 4,916评论 0 23
  • 写在前面 每次使用KVO和通知我就觉得是一件麻烦的事情,即便谈不上麻烦,也可说是不方便吧,对于KVO,你需要注册,...
    zhong_JF阅读 484评论 1 3
  • 本文结构如下: Why? (为什么要用KVO) What? (KVO是什么) How? ( KVO怎么用) Mo...
    等开会阅读 1,636评论 1 21
  • 大白健康系统--iOS APP运行时Crash自动修复系统 前言 大白(Baymax),迪士尼动画《超能陆战队》中...
    鼠犬玉阅读 17,423评论 22 158