1. 程式人生 > >iOS KVO crash 自修復技術實現與原理解析

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

圖片描述

- (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 這樣就完全不干擾我們平時的程式碼書寫邏輯了。