Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ChenYilong opened this issue Jun 11, 2018 · 0 comments
Open

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

ChenYilong opened this issue Jun 11, 2018 · 0 comments
Labels

Comments

@ChenYilong
Copy link
Owner

ChenYilong commented Jun 11, 2018

前言

【前言】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

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x7f8827d21d20 of class XXXX was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x61000003db00> (
<NSKeyValueObservance 0x61000025ae80: Observer: 0x7f882890b4c0, Key path: dataSource, Options: <New: YES, Old: NO, Prior: NO> Context: 0x10dfe7730, Property: 0x61000025b810>
)'
*** First throw call stack:
(
   0   CoreFoundation                      0x00000001102b0b0b __exceptionPreprocess + 171
   1   libobjc.A.dylib                     0x00000001167eb141 objc_exception_throw + 48
   2   CoreFoundation                      0x0000000110319625 +[NSException raise:format:] + 197
   3   Foundation                          0x0000000111322b53 NSKVODeallocate + 294
   4   UIKit                               0x00000001138ec544 __destroy_helper_block_.125 + 80
   5   libsystem_blocks.dylib              0x00000001185a999d _Block_release + 111
   6   UIKit                               0x00000001139bd187 -[UIViewAnimationBlockDelegate .cxx_destruct] + 43
   7   libobjc.A.dylib                     0x00000001167e99bc _ZL27object_cxxDestructFromClassP11objc_objectP10objc_class + 127
   8   libobjc.A.dylib                     0x00000001167f5d34 objc_destructInstance + 129
   9   libobjc.A.dylib                     0x00000001167f5d66 object_dispose + 22
   10  libobjc.A.dylib                     0x00000001167ffb8e _ZN11objc_object17sidetable_releaseEb + 202
   11  CoreFoundation                      0x000000011021952d -[__NSDictionaryI dealloc] + 125
   12  libobjc.A.dylib                     0x00000001167ffb8e _ZN11objc_object17sidetable_releaseEb + 202
   13  libobjc.A.dylib                     0x00000001168002fa _ZN12_GLOBAL__N_119AutoreleasePoolPage3popEPv + 866
   14  CoreFoundation                      0x00000001101ffe96 _CFAutoreleasePoolPop + 22
   15  CoreFoundation                      0x000000011023baec __CFRunLoopRun + 2172
   16  CoreFoundation                      0x000000011023b016 CFRunLoopRunSpecific + 406
   17  GraphicsServices                    0x0000000118f1ea24 GSEventRunModal + 62
   18  UIKit                               0x0000000113904134 UIApplicationMain + 159
   19  HaiDiLao                            0x000000010d50b5ef main + 111
   20  libdyld.dylib                       0x000000011856265d start + 1
   21  ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

防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 这样就完全不干扰我们平时的代码书写逻辑了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant