iOS KVO crash 自修复技术实现与原理解析

简介: 【前言】KVO API设计非常不合理,于是有很多的KVO三方库,比如 KVOController 用更优的API来规避这些crash,但是侵入性比较大,必须编码规范来约束所有人都要使用该方式。有没有什么更优雅,无感知的接入方式?

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方法:

原函数 swizzle后的函数
addObserver:forKeyPath:options:context: cyl_crashProtectaddObserver:forKeyPath:options:context:
removeObserver:forKeyPath: cyl_crashProtectremoveObserver:forKeyPath:
removeObserver:forKeyPath:context: cyl_crashProtectremoveObserver:forKeyPath:context:

- (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,同时也可以写,
同时也可以多次 addObserverremoveObserver 这样就完全不干扰我们平时的代码书写逻辑了。

相关文章
|
13小时前
|
负载均衡 网络协议 容灾
【飞天技术沙龙】云解析DNS上海站《多云+IDC融合场景下的DNS最佳实践》圆满落幕
【飞天技术沙龙】云解析DNS上海站《多云+IDC融合场景下的DNS最佳实践》于5月22日在上海举行,吸引了20余家知名企业参与。本次沙龙聚集“多云+IDC的融合DNS场景”,从DNS在“企业上云”和“容灾调度”两个主题的最佳实践展开分享,沙龙活动取得了圆满成功。
|
1天前
|
安全 算法 网络安全
网络安全与信息安全:防护之道与实战策略网络防线的构筑者:网络安全与信息保护技术解析
【5月更文挑战第27天】 在数字化时代,数据成为了新的货币,而网络安全则是保护这些宝贵资产不受威胁的盾牌。本文将深入探讨网络安全漏洞的概念、加密技术的最新进展以及提升个人和企业的安全意识。通过对网络攻击者的策略进行剖析,我们不仅揭示了常见的安全漏洞,还分享了如何通过多层次防御机制来增强系统的安全性。文章的目标是为读者提供实用的知识,以便构建一个更加坚固的网络安全防线。
|
1天前
|
域名解析 负载均衡 网络协议
【域名解析DNS专栏】DNS解析中的Anycast技术:原理与优势
【5月更文挑战第27天】Anycast技术是解决DNS解析高效、稳定和安全问题的关键。它将一个IP地址分配给多地服务器,客户端请求自动路由至最近的低负载服务器,减少延迟,提高解析速度。此外,Anycast实现负载均衡,缓解DDoS攻击,并确保高可用性。通过遍历Anycast服务器选择最低延迟者进行DNS解析,实现网络性能优化。随着技术发展,Anycast在DNS解析中的应用将更加广泛。
|
3天前
|
域名解析 网络协议 安全
【域名解析 DNS 专栏】动态 DNS(DDNS)技术解析及其应用
【域名解析 DNS 专栏】动态 DNS(DDNS)技术解析及其应用 动态DNS(DDNS)技术在应对动态IP地址环境下,提供了一种灵活的解决方案,使设备能通过固定域名被访问。当设备IP改变时,DDNS服务会更新域名与新IP的映射,确保访问畅通。广泛应用于家庭远程访问设备和企业网络管理。简单的DDNS更新Python示例展示了发送请求更新过程。然而,DDNS面临服务可靠性和安全性的挑战。总体而言,DDNS技术提升了网络环境的便利性和效率,并将持续发展和完善。
|
4天前
|
Java 编译器 开发者
Java注解(Annotation)技术深入解析
Java注解(Annotation)技术深入解析
305 1
|
4天前
|
设计模式 算法 Java
Java语言中的接口技术深入解析
Java语言中的接口技术深入解析
189 0
|
4天前
|
Java
Java语言中的继承技术深入解析
Java语言中的继承技术深入解析
160 0
|
4天前
|
安全 Java
Java语言中的封装技术深入解析
Java语言中的封装技术深入解析
113 1
|
4天前
|
存储 Java
Java语言中的多态技术深入解析
Java语言中的多态技术深入解析
155 0
|
4天前
|
存储 安全 Java
Java泛型:原理、应用与深入解析
Java泛型:原理、应用与深入解析

推荐镜像

更多