[资料] iOS KVO crash 自修复技术实现与原理解析

[复制链接]

助理工程师

发表于 2018-2-8 12:30:06   477 查看 0 回复 显示全部楼层 倒序浏览
分享
摘要: 【前言】KVO API设计非常不合理,于是有很多的KVO三方库,比如 KVOController 用更优的API来规避这些crash,但是侵入性比较大,必须编码规范来约束所有人都要使用该方式。有没有什么更优雅,无感知的接入方式?
点此查看原文:http://click.aliyun.com/m/41952/
KVO crash 自修复技术实现与原理解析
前言
【前言】KVO API设计非常不合理,于是有很多的KVO三方库,比如 KVOController用更优的API来规避这些crash,但是侵入性比较大,必须编码规范来约束所有人都要使用该方式。有没有什么更优雅,无感知的接入方式?
简介
KVO crash 也是非常常见的 Crash 类型,在探讨 KVO crash 原因前,我们先来看一下传统的KVO写发:

#warning move this to top of .m file//#define MyKVOContext(A) static void * const A = (void*)&A;static void * const MyContext = (void*)&MyContext;#warning move this to viewdidload or init method    // KVO注册监听:   // _A 监听 _B  的 @"keyPath"  属性   //[self.B  addObserver: self.A forKeyPath:@"keyPath" options:NSKeyValueObservingOptionNew context:MyContext];- (void)dealloc {   // KVO反注册   [_B removeObserver:_A forKeyPath:@"keyPath"];}// KVO监听执行 #warning — please move this method to  the class of _A  - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {   if(context != MyContext) {       [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];       return;   }   if(context == MyContext) {   //if ([keyPath isEqualToString:@"keyPath"]) {       id newKey = change[NSKeyValueChangeNewKey];       BOOL boolValue = [newKey boolValue];   }}
看到如上的写发,大概我们就明白了 API 设计不合理的地方:
B 需要做的工作太多,B可能引起Crash的点也太多:
B 需要主动移除监听者的时机,否则就crash:
B 在释放变为nil后,hook dealloc时机
A 在释放变为nil后 否则报错 Objective-C Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
KVO的被观察者dealloc时仍然注册着KVO导致的crash
B 不能移除监听者A的时机,否则就crash:
B没有被A监听
B已经移除A的监听。
添加KVO重复添加观察者或重复移除观察者(KVO 注册观察者与移除观察者不匹配)导致的crash。
采取的措施:
B添加A监听的时候,避免重复添加,移除的时候避免重复移除。
B dealloc时及时移除 A
A dealloc时,让 B 移除A。
避免重复添加,避免重复移除。
报错信息一览:

2018-01-24 16:08:54.100667+0800 BootingProtection[63487:29487624] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<CYLObserverView: 0x7fb287002fb0; frame = (0 0; 207 368); layer = <CALayer: 0x604000039360>>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
防crash措施
于是有很多的KVO三方库,比如 KVOController 用更优的API来规避这些crash,但是侵入性比较大,必须编码规范来约束所有人都要使用该方式。有没有什么更优雅,无感知的接入方式?
那便是我们下面要讲的 KVO crash 防护机制。
我们可以对比下其他的一些KVO防护方案:
网络上有一些类似的方案,“大白健康系统”方案大致如下:
KVO的被观察者dealloc时仍然注册着KVO导致的crash 的情况,可以将NSObject的dealloc swizzle, 在object dealloc的时候自动将其对应的kvodelegate所有和kvo相关的数据清空,然后将kvodelegate也置空。避免出现KVO的被观察者dealloc时仍然注册着KVO而产生的crash
这样未免太过麻烦,我们可以借助第三方库 CYLDeallocBlockExecutor hook 任意一个对象的 dealloc 时机,然后在 dealloc 前进行我们需要进行的操作,因此也就不需要为 NSObject 加 flag 来进行全局的筛选。flag 效率非常底,影响 app 性能。
“大白健康系统”思路是建立一个delegate,观察者和被观察者通过delegate间接建立联系,由于没有demo源码,这种方案比较繁琐。可以考虑建立一个哈希表,用来保存观察者、keyPath的信息,如果哈希表里已经有了相关的观察者,keyPath信息,那么继续添加观察者的话,就不载进行添加,同样移除观察的时候,也现在哈希表中进行查找,如果存在观察者,keypath信息,那么移除,如果没有的话就不执行相关的移除操作。要实现这样的思路就需要用到methodSwizzle来进行方法交换。我这通过写了一个NSObject的cagegory来进行方法交换。示例代码如下:
下面是核心的swizzle方法:

- (void)cyl_crashProtectaddObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{   if (!observer || !keyPath || keyPath.length == 0) {       return;   }   @synchronized (self) {       NSInteger kvoHash = [self _cyl_crashProtectHash:observer :keyPath];       if (!self.KVOHashTable) {           self.KVOHashTable = [NSHashTable hashTableWithOptions:NSPointerFunctionsStrongMemory];       }       if (![self.KVOHashTable containsObject:@(kvoHash)]) {           [self.KVOHashTable addObject:@(kvoHash)];           [self cyl_crashProtectaddObserver:observer forKeyPath:keyPath options:options context:context];           [self cyl_willDeallocWithSelfCallback:^(__unsafe_unretained id observedOwner, NSUInteger identifier) {               [observedOwner cyl_crashProtectremoveObserver:observer forKeyPath:keyPath context:context];           }];           __unsafe_unretained typeof(self) unsafeUnretainedSelf = self;           [observer cyl_willDeallocWithSelfCallback:^(__unsafe_unretained id observerOwner, NSUInteger identifier) {               [unsafeUnretainedSelf cyl_crashProtectremoveObserver:observerOwner forKeyPath:keyPath context:context];           }];       }   }}- (void)cyl_crashProtectremoveObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context {   //TODO:  加上 context 限制,防止父类、子类使用同一个keyPath。   [self cyl_crashProtectremoveObserver:observer forKeyPath:keyPath];}- (void)cyl_crashProtectremoveObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{   //TODO:  white list   if (!observer || !keyPath || keyPath.length == 0) {       return;   }   @synchronized (self) {       if (!observer) {           return;       }       NSInteger kvoHash = [self _cyl_crashProtectHash:observer :keyPath];       NSHashTable *hashTable = [self KVOHashTable];       if (!hashTable) {           return;       }       if ([hashTable containsObject:@(kvoHash)]) {           [self cyl_crashProtectremoveObserver:observer forKeyPath:keyPath];           [hashTable removeObject:@(kvoHash)];       }   }}
之后我们就可以模拟dealloc中不写removeObserver,同时也可以写,
同时也可以多次 addObserver、removeObserver 这样就完全不干扰我们平时的代码书写逻辑了。
扫码获取更多资讯:






标签:iOS 技术 原理 解析

只有小组成员才能发言,加入小组>>

157个成员聚集在这个小组

加入小组

创建小组步骤

关闭

站长推荐 上一条 /8 下一条

快速回复 返回顶部 返回列表
-

推荐专区

技术干货集中营

专家问答

用户帮助┃咨询与建议┃版主议事

工程师杂谈

工程师创意

工程师职场

论坛电子赛事

社区活动专版

发烧友活动

-

嵌入式论坛

ARM技术论坛

Android论坛

Linux论坛

单片机/MCU论坛

FPGA|CPLD|ASIC论坛

DSP论坛

嵌入式系统论坛

-

电源技术论坛

电源技术论坛

无线充电技术

-

硬件设计论坛

PCB设计论坛

电路设计论坛

电子元器件论坛

控制|传感

总线技术|接口技术

-

测试测量论坛

LabVIEW论坛

Matlab论坛

测试测量技术专区

仪器仪表技术专区

-

EDA设计论坛

multisim论坛

PADS技术论坛

Protel|AD|DXP论坛

Allegro论坛

proteus论坛|仿真论坛

EasyEDA-中国人自已的EDA工具

Orcad论坛

-

综合技术与应用

电机控制

智能电网

光电及显示

参考设计中心

汽车电子技术论坛

医疗电子论坛

-

开源硬件

-

无线通信论坛

无线通信技术专区

天线|RF射频|微波|雷达技术

-

IC设计论坛

芯片测试与失效分析

Mixed Signal/SOC[数模混合芯片设计]

Analog/RF IC设计

设计与制造封装测试

-

厂商专区

TI论坛

TI Deyisupport社区

-

检测技术与质量

电磁兼容(EMC)设计与整改

安规知识论坛

检测与认证

-

消费电子论坛

手机技术论坛

平板电脑/mid论坛

音视/视频/机顶盒论坛

-

电子论坛综合区

聚丰众筹官方社区

新人报道区

聚丰供应链

-

论坛服务区

-

供求信息发布

供需广告

招聘┃求职发布区

电子展览展会专区