完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
摘要: 【前言】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: ' 防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 这样就完全不干扰我们平时的代码书写逻辑了。 扫码获取更多资讯: |
|
|
|
只有小组成员才能发言,加入小组>>
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-12-25 18:42 , Processed in 0.735198 second(s), Total 70, Slave 51 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号