1缘圈、前言
??閃退劣光,指用戶在使用app的過程中異常中斷執(zhí)行,被系統(tǒng)強制結(jié)束應(yīng)用并回到桌面糟把。不僅內(nèi)存信息丟失绢涡,還會阻斷用戶操作流程,對業(yè)務(wù)影響及其嚴重遣疯。
??當然雄可,避免崩潰的最好辦法就是不產(chǎn)生崩潰;在開發(fā)過各中就要盡可能地保證程序的健壯性。但是数苫,人又不是機器聪舒,不可能不犯錯,不可能存在沒有BUG的程序虐急。
??如果能夠利用一些語言機制和系統(tǒng)方法箱残,設(shè)計一套防護系統(tǒng),使之能夠有效的降低APP的崩潰率止吁,那么不僅APP的穩(wěn)定性得到了保障被辑,最重要的是可以減少不必要的加班。
??當然我們不可能強大到把所有類型的crash都處理掉敬惦,但是我們會對一些高頻的crash進行一一的處理盼理,我們的目的就是降低crash率。
2俄删、對RunTime的初識
??C
語言作為一門靜態(tài)類語言宏怔,在編譯階段就已經(jīng)確定了所有變量的數(shù)據(jù)類型,同時也確定好了要調(diào)用的函數(shù)抗蠢,以及函數(shù)的實現(xiàn)举哟。
?? 而Objective-C
語言是一門動態(tài)語言。在編譯階段并不知道變量的具體數(shù)據(jù)類型迅矛,也不知道所真正調(diào)用的哪個函數(shù)妨猩;在編譯階段,OC可以調(diào)用任何函數(shù)秽褒,即使這個函數(shù)只聲明未實現(xiàn)壶硅,或直接未聲明(performSelector:)
;只有在運行時間才檢查變量的數(shù)據(jù)類型销斟,同時在運行時才會根據(jù)函數(shù)名查找要調(diào)用的具體函數(shù)庐椒。這樣在程序沒運行的時候,我們并不知道調(diào)用一個方法具體會發(fā)生什么蚂踊。
??實現(xiàn)Objective-C
語言運行時機制的一切基礎(chǔ)就是Runtime
约谈。Runtime實際上是一個庫,這個庫使我們可以在程序運行時動態(tài)的創(chuàng)建對象犁钟、檢查對象棱诱,修改類和對象的方法。
3涝动、消息機制的基本原理
? ?Objective-C
語言中迈勋,方法調(diào)用都是類似[receiver selector];
的形式,其本質(zhì)就是讓對象在運行時發(fā)送消息的過程醋粟。
我們來看看方法調(diào)用[receiver selector];
在『編譯階段』
和『運行階段』
分別做了什么靡菇?
- 編譯階段:
[receiver selector];
方法被編譯器轉(zhuǎn)換為:
?1.1.objc_msgSend(receiver重归,selector) (不帶參數(shù))
?1.2.objc_msgSend(recevier,selector厦凤,org1鼻吮,org2,…)(帶參數(shù))
OC代碼示例:
id num = @123;
NSMutableString *str = [NSMutableString stringWithString:@"Hello"];
[str appendString:@"World"];
通過clang
命令查看OC代碼編譯后的樣子:
//xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
id num = ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 123);
NSMutableString *str = ((NSMutableString * _Nonnull (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("stringWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_zh_hdyn1j_92zn2n74srg5dq88c0000gp_T_main_0eed7d_mi_1);
((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)str, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_zh_hdyn1j_92zn2n74srg5dq88c0000gp_T_main_0eed7d_mi_2);
- 運行時階段:消息接受者
recevier
尋找對應(yīng)的selector
泳唠。
2.1狈网、通過recevier
的isa
指針找到recevier
的Class(類)
;
2.2笨腥、在Class(類)
的cache(方法緩存)
的散列表中尋找對應(yīng)的IMP(方法實現(xiàn))
拓哺;
2.3、如果在cache(方法緩存)
中沒有找到對應(yīng)的IMP(方法實現(xiàn))
的話脖母,就繼續(xù)在Class(類)
的method list(方法列表)
中找對應(yīng)的selector
士鸥,如果找到,填充到cache(方法緩存)
中谆级,并返回selector
烤礁;
2.4、如果在Class(類)
中沒有找到這個selector
肥照,就繼續(xù)在它的superClass(父類)
中尋找脚仔,以此類推,一直查找到根類舆绎;
2.5鲤脏、一旦找到對應(yīng)的selector
,直接執(zhí)行recevier
對應(yīng)selector
方法實現(xiàn)的IMP(方法實現(xiàn))
吕朵。
2.6猎醇、若找不到對應(yīng)的selector
,轉(zhuǎn)向攔截調(diào)用努溃,走消息轉(zhuǎn)發(fā);如果沒有重寫攔截調(diào)用的方法,程序就會崩潰硫嘶。
4、防護原理
? ?利用Objective-C
語言的動態(tài)特性梧税,采用AOP(Aspect Oriented Programming)
面向切面編程的設(shè)計思想沦疾,在不侵入原有項目代碼的基礎(chǔ)之上,通過在 APP 運行時階段對崩潰因素的的攔截和處理第队,使得 APP 能夠持續(xù)穩(wěn)定正常的運行哮塞。
? ?具象的說,就是對需要Hook的類添加Category(分類)
斥铺,在各個分類中通過Method Swizzling
攔截容易造成崩潰的系統(tǒng)方法彻桃,將系統(tǒng)原有方法與添加的防護方法的selector(方法選擇器)
與IMP(函數(shù)實現(xiàn)指針)
進行對調(diào)坛善。然后在替換方法中添加防護操作晾蜘,從而達到避免以及修復(fù)崩潰的目的邻眷。
5、防護系統(tǒng)可以處理掉哪幾種crash類型剔交?
- unrecognized selector(找不到對象方法或者類方法的實現(xiàn))
- KVO Crash
- KVC Crash
- NSNotification Crash
- NSTimer Crash (注冊了沒有主動釋放會內(nèi)存泄露肆饶,甚至在定時任務(wù)觸發(fā)時可能會導(dǎo)致crash)
- Container / NSString Crash(集合類操作造成的崩潰,例如數(shù)組越界岖常,插入 nil 等)
- Threading Crash (非主線程刷新UI)
5.1 unrecognized selector類型crash防護
? ?unrecognized selector
類型的crash在app眾多的crash類型中占著比較大的成分驯镊,通常是因為一個對象調(diào)用了一個不屬于它方法的方法導(dǎo)致的。
部分復(fù)現(xiàn)代碼:
//.h中聲明了竭鞍,但是沒有實現(xiàn)此方法
[self gg];
//performSelector可以向一個對象傳遞任何消息板惑,而不需要在編譯的時候聲明這些方法
[self performSelector:@selector(gg1)];
[self performSelector:NSSelectorFromString(@"gg2:")];
//id能代表任意類型對象,在編譯期會跳過類型檢查
id str = @123;
[str appendString:@"Hello World"];
//類型不匹配導(dǎo)致崩潰
[self gg3:@123];
- (void)gg3:(NSString *)str
{
//沒有對參數(shù)進行類型判斷
NSInteger length = str.length;
}
? ?在3偎快、消息機制的基本原理 最后一步中有提到:若找不到對應(yīng)的selector冯乘,轉(zhuǎn)向攔截調(diào)用,走消息轉(zhuǎn)發(fā);如果沒有重寫攔截調(diào)用的方法,程序就會崩潰
晒夹。
? ?當一個方法找不到的時候裆馒,Runtime 提供了消息動態(tài)解析
、消息接受者重定向
丐怯、消息重定向
等三步處理消息喷好,具體流程如下:
圖解:
消息動態(tài)解析
:Objective-C
運行時會調(diào)用+resolveInstanceMethod:
或者 +resolveClassMethod:
,讓你有機會提供一個函數(shù)實現(xiàn)读跷。我們可以通過重寫這兩個方法梗搅,添加其他函數(shù)實現(xiàn),并返回 YES舔亭, 那運行時系統(tǒng)就會重新啟動一次消息發(fā)送的過程些膨。若返回NO或者沒有添加其他函數(shù)實現(xiàn),則進入下一步钦铺。
舉個例子:
#import "ViewController.h"
#include <objc/runtime.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 執(zhí)行 fun 函數(shù)
[self performSelector:@selector(fun)];
}
// 重寫 resolveInstanceMethod: 添加對象方法實現(xiàn)
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(fun)) { // 如果是執(zhí)行 fun 函數(shù)订雾,就動態(tài)解析,指定新的 IMP
//特殊參數(shù):v@:,具體可參考官方文檔:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
class_addMethod([self class], sel, (IMP)funMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void funMethod(id obj, SEL _cmd) {
NSLog(@"funMethod"); //新的 fun 函數(shù)
}
控制臺打印結(jié)果:
2021-03-30 16:50:06.145815+0800 CrashKiller_Example[22017:4869725] funMethod
從上邊例子中可以看出矛洞,雖然我們沒有實現(xiàn) fun 方法沼本,但是通過重寫resolveInstanceMethod: ,
利用 class_addMethod 方法添加對象方法實現(xiàn) funMethod 方法抽兆,并執(zhí)行。
從打印結(jié)果來看辫红,成功調(diào)起了funMethod 方法祝辣。
消息接受者重定向
:如果當前對象實現(xiàn)了forwardingTargetForSelector:
,Runtime
就會調(diào)用這個方法切油,允許我們將消息的接受者轉(zhuǎn)發(fā)給其他對象澎胡。如果這一步方法返回 nil攻谁,則進入下一步。
舉個例子:
#import "ViewController.h"
#include "objc/runtime.h"
@interface Person : NSObject
- (void)fun;
@end
@implementation Person
- (void)fun {
NSLog(@"fun");
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 執(zhí)行 fun 方法
[self performSelector:@selector(fun)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES; // 為了進行下一步 消息接受者重定向
}
// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(fun)) {
return [[Person alloc] init];
// 返回 Person 對象槐瑞,讓 Person 對象接收這個消息
}
return [super forwardingTargetForSelector:aSelector];
}
控制臺打印結(jié)果:
2021-03-30 17:10:52.645582+0800 CrashKiller_Example[22427:4885650] fun
可以看到困檩,雖然當前 ViewController 沒有實現(xiàn) fun 方法+resolveInstanceMethod:也沒有添加其他函數(shù)實現(xiàn)悼沿。
但是我們通過 forwardingTargetForSelector 把當前 ViewController 的方法轉(zhuǎn)發(fā)給了 Person 對象去執(zhí)行了糟趾。
打印結(jié)果也證明我們成功實現(xiàn)了轉(zhuǎn)發(fā)义郑。
消息重定向
:Runtime
系統(tǒng)利用methodSignatureForSelector:
方法獲取函數(shù)的參數(shù)和返回值類型丈钙。
- 如果
methodSignatureForSelector:
返回了一個NSMethodSignature
對象(函數(shù)簽名)雏赦,Runtime
系統(tǒng)就會創(chuàng)建一個NSInvocation
對象星岗,并通過forwardInvocation:
消息通知當前對象俏橘,給予此次消息發(fā)送最后一次尋找IMP
的機會。 - 如果
methodSignatureForSelector:
返回 nil汉额。則Runtime
系統(tǒng)會發(fā)出doesNotRecognizeSelector:
消息榨汤,程序也就崩潰了收壕。
舉個例子:
#import "ViewController.h"
#include "objc/runtime.h"
@interface Person : NSObject
- (void)fun;
@end
@implementation Person
- (void)fun {
NSLog(@"fun");
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 執(zhí)行 fun 函數(shù)
[self performSelector:@selector(fun)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES; // 為了進行下一步 消息接受者重定向
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil; // 為了進行下一步 消息重定向
}
// 獲取函數(shù)的參數(shù)和返回值類型蜜宪,返回簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"fun"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector; // 從 anInvocation 中獲取消息
Person *p = [[Person alloc] init];
if([p respondsToSelector:sel]) { // 判斷 Person 對象方法是否可以響應(yīng) sel
[anInvocation invokeWithTarget:p]; // 若可以響應(yīng)圃验,則將消息轉(zhuǎn)發(fā)給其他對象處理
} else {
[self doesNotRecognizeSelector:sel]; // 若仍然無法響應(yīng)澳窑,則報錯:找不到響應(yīng)方法
}
}
@end
控制臺打印結(jié)果:
2021-03-30 17:26:26.719413+0800 CrashKiller_Example[22715:4896625] fun
從補救三部曲中摊聋,選擇哪一步作為入手點最合適麻裁?
? ?1. resolveInstanceMethod
需要在類的本身動態(tài)的添加它本身不存在的方法煎源,這些方法對于該類本身來說是冗余的
? ?2. forwardInvocation
可以通過NSInvocation
的形式將消息轉(zhuǎn)發(fā)給多個對象手销,但是其開銷比較大图张,需要創(chuàng)建新的NSInvocation
對象埂淮,并且forwardInvocation
的函數(shù)經(jīng)常被使用者調(diào)用來做消息的轉(zhuǎn)發(fā)選擇機制倔撞,不適合多次重寫痪蝇。
? ?3. forwardingTargetForSelector
可以將消息轉(zhuǎn)發(fā)給一個對象,開銷較小耙册,并且被重寫的概率較低毫捣,適合重寫蔓同。
具體步驟如下:
? ?1. 給NSObject
添加一個分類斑粱,在分類中實現(xiàn)一個自定義的-crashKiller_forwardingTargetForSelector:
方法则北;
? ?2. 利用Method Swizzling
將-forwardingTargetForSelector:
和 -crashKiller_forwardingTargetForSelector:
進行方法交換尚揣。
? ?3. 在自定義的方法中惑艇,先判斷當前對象是否已經(jīng)實現(xiàn)了消息接受者重定向
和消息重定向
滨巴。如果都沒有實現(xiàn)恭取,就動態(tài)創(chuàng)建一個目標類蜈垮,給目標類動態(tài)添加一個方法。
? ?4. 把消息轉(zhuǎn)發(fā)給動態(tài)生成類的實例對象调塌,由目標類動態(tài)創(chuàng)建的方法實現(xiàn)羔砾,這樣 APP 就不會崩潰了姜凄。
實現(xiàn)代碼如下:
#import "NSObject+KillSelector.h"
#import "NSObject+CrashKillerMethodSwizzling.h"
@implementation NSObject (KillSelector)
+ (void)registerKillSelector
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 攔截 `+forwardingTargetForSelector:` 方法态秧,替換自定義實現(xiàn)
[NSObject crashKillerMethodSwizzlingClassMethod:@selector(forwardingTargetForSelector:)
withMethod:@selector(crashKiller_forwardingTargetForSelector:)
withClass:[NSObject class]];
// 攔截 `-forwardingTargetForSelector:` 方法申鱼,替換自定義實現(xiàn)
[NSObject crashKillerMethodSwizzlingInstanceMethod:@selector(forwardingTargetForSelector:)
withMethod:@selector(crashKiller_forwardingTargetForSelector:)
withClass:[NSObject class]];
});
}
+ (id)crashKiller_forwardingTargetForSelector:(SEL)aSelector {
if ([[CrashKillerManager shareManager].selectorClassWhiteList containsObject:[self class]]) {
return [self crashKiller_forwardingTargetForSelector:aSelector];
}
SEL forwarding_sel = @selector(forwardingTargetForSelector:);
// 獲取 NSObject 的消息轉(zhuǎn)發(fā)方法
Method root_forwarding_method = class_getClassMethod([NSObject class], forwarding_sel);
// 獲取 當前類 的消息轉(zhuǎn)發(fā)方法
Method current_forwarding_method = class_getClassMethod([self class], forwarding_sel);
// 判斷當前類本身是否實現(xiàn)第二步:消息接受者重定向
BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);
// 如果沒有實現(xiàn)第二步:消息接受者重定向
if (!realize) {
// 判斷有沒有實現(xiàn)第三步:消息重定向
SEL methodSignature_sel = @selector(methodSignatureForSelector:);
Method root_methodSignature_method = class_getClassMethod([NSObject class], methodSignature_sel);
Method current_methodSignature_method = class_getClassMethod([self class], methodSignature_sel);
realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);
// 如果沒有實現(xiàn)第三步:消息重定向
if (!realize) {
// 創(chuàng)建一個新類
NSString *errClassName = NSStringFromClass([self class]);
NSString *errSel = NSStringFromSelector(aSelector);
NSString *reason = [NSString stringWithFormat:@"-[%@ %@]: unrecognized selector sent to class %p",errClassName,errSel,self];
[[CrashKillerManager shareManager] throwExceptionWithName:@"NSInvalidArgumentException" reason:reason];
NSString *className = @"CrachClass";
Class cls = NSClassFromString(className);
// 如果類不存在 動態(tài)創(chuàng)建一個類
if (!cls) {
Class superClsss = [NSObject class];
cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
// 注冊類
objc_registerClassPair(cls);
}
// 如果類沒有對應(yīng)的方法,則動態(tài)添加一個
if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
class_addMethod(cls, aSelector, (IMP)CrashIMP, "@@:@");
}
// 把消息轉(zhuǎn)發(fā)到當前動態(tài)生成類的實例對象上
return [[cls alloc] init];
}
}
return [self crashKiller_forwardingTargetForSelector:aSelector];
}
- (id)crashKiller_forwardingTargetForSelector:(SEL)aSelector {
if ([[CrashKillerManager shareManager].selectorClassWhiteList containsObject:[self class]]) {
return [self crashKiller_forwardingTargetForSelector:aSelector];
}
SEL forwarding_sel = @selector(forwardingTargetForSelector:);
// 獲取 NSObject 的消息轉(zhuǎn)發(fā)方法
Method root_forwarding_method = class_getInstanceMethod([NSObject class], forwarding_sel);
// 獲取 當前類 的消息轉(zhuǎn)發(fā)方法
Method current_forwarding_method = class_getInstanceMethod([self class], forwarding_sel);
// 判斷當前類本身是否實現(xiàn)第二步:消息接受者重定向
BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);
// 如果沒有實現(xiàn)第二步:消息接受者重定向
if (!realize) {
// 判斷有沒有實現(xiàn)第三步:消息重定向
SEL methodSignature_sel = @selector(methodSignatureForSelector:);
Method root_methodSignature_method = class_getInstanceMethod([NSObject class], methodSignature_sel);
Method current_methodSignature_method = class_getInstanceMethod([self class], methodSignature_sel);
realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);
// 如果沒有實現(xiàn)第三步:消息重定向
if (!realize) {
// 創(chuàng)建一個新類
NSString *errClassName = NSStringFromClass([self class]);
NSString *errSel = NSStringFromSelector(aSelector);
NSString *reason = [NSString stringWithFormat:@"-[%@ %@]: unrecognized selector sent to instance %p",errClassName,errSel,self];
[[CrashKillerManager shareManager] throwExceptionWithName:@"NSInvalidArgumentException" reason:reason];
NSString *className = @"CrachClass";
Class cls = NSClassFromString(className);
// 如果類不存在 動態(tài)創(chuàng)建一個類
if (!cls) {
Class superClsss = [NSObject class];
cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
// 注冊類
objc_registerClassPair(cls);
}
// 如果類沒有對應(yīng)的方法竿痰,則動態(tài)添加一個
if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
class_addMethod(cls, aSelector, (IMP)CrashIMP, "@@:@");
}
// 把消息轉(zhuǎn)發(fā)到當前動態(tài)生成類的實例對象上
return [[cls alloc] init];
}
}
return [self crashKiller_forwardingTargetForSelector:aSelector];
}
// 動態(tài)添加的方法實現(xiàn)
static int CrashIMP(id slf, SEL selector) {
return 0;
}
@end
5.2 KVO Crash類型crash防護
KVO(Key-Value Observing)影涉,它提供一種機制蟹倾,當指定的對象的屬性被修改后鲜棠,KVO就會自動通知相應(yīng)的觀察者。
通常一個對象的KVO關(guān)系圖如下:
[self.wkWebView addObserver:self
forKeyPath:@"estimatedProgress"
options:NSKeyValueObservingOptionNew
context:nil];
[self.wkWebView addObserver:self
forKeyPath:@"title"
options:NSKeyValueObservingOptionNew
context:nil];
KVO Crash常見原因:
??1. KVO 添加次數(shù)和移除次數(shù)不匹配:
??????移除未注冊的觀察者,導(dǎo)致崩潰
??????重復(fù)移除多次表鳍,移除次數(shù)多于添加次數(shù)譬圣,導(dǎo)致崩潰
??????重復(fù)添加多次胁镐,雖然不會崩潰,但是發(fā)生改變時颇玷,也同時會被觀察多次帖渠。
??2. 被觀察者提前被釋放空郊,被觀察者在 dealloc 時仍然注冊著 KVO狞甚,導(dǎo)致崩潰哼审。 例如:被觀察者是局部變量的情況(iOS 10 及之前會崩潰)涩盾。
??3. 添加了觀察者春霍,但未實現(xiàn) observeValueForKeyPath:ofObject:change:context: 方法址儒,導(dǎo)致崩潰衅疙。
??4. 添加或者移除時 keypath == nil炼蛤,導(dǎo)致崩潰理朋。
解決方案:
??1. 首先為 NSObject 建立一個分類嗽上,利用 Method Swizzling兽愤,實現(xiàn)自定義的 crashKiller_addObserver:forKeyPath:options:context:
、crashKiller_removeObserver:forKeyPath:
crashKiller_removeObserver:forKeyPath:context:
哲思、
crashKiller_dealloc
方法棚赔,用來替換系統(tǒng)原生的添加移除觀察者方法的實現(xiàn)靠益。
??2. 然后在觀察者和被觀察者之間建立一個 KVOProxy對象胧后,兩者之間通過KVOProxy對象建立聯(lián)系壳快。然后在添加和移除操作時江醇,將 KVO 的相關(guān)信息例如 observer陶夜、keyPath保存進關(guān)系哈希表kvoInfoMap中条辟。 關(guān)系哈希表的數(shù)據(jù)結(jié)構(gòu):{keypath : observer1, observer2 , ...}
??3. 在添加和移除操作的時候羽嫡,利用KVOProxy對象做轉(zhuǎn)發(fā)杭棵,把真正的觀察者變?yōu)?KVOProxy對象魂爪,而當被觀察者的特定屬性發(fā)生了改變滓侍,再由 KVOProxy對象 分發(fā)到原有的觀察者上撩笆。
添加觀察者時: 通過關(guān)系哈希表判斷是否重復(fù)添加夕冲,只添加一次耘擂。
移除觀察者時: 通過關(guān)系哈希表是否已經(jīng)進行過移除操作醉冤,避免多次移除蚁阳。
觀察鍵值改變時: 同樣通過關(guān)系哈希表判斷螺捐,將改變操作分發(fā)到原有的觀察者上定血。
??另外澜沟,為了避免被觀察者提前被釋放茫虽,被觀察者在 dealloc 時仍然注冊著 KVO 導(dǎo)致崩潰濒析。防護系統(tǒng)還利用 Method Swizzling 實現(xiàn)了自定義的 dealloc号杏,在系統(tǒng) dealloc 調(diào)用之前盾致,將多余的觀察者移除掉绰上。
KVO崩潰測試代碼:
/**
1.1 移除了未注冊的觀察者蜈块,導(dǎo)致崩潰
*/
- (void)testKVOCrash11 {
// 崩潰日志:Cannot remove an observer XXX for the key path "xxx" from XXX because it is not registered as an observer.
[self.objc removeObserver:self forKeyPath:@"name"];
}
/**
1.2 重復(fù)移除多次爽哎,移除次數(shù)多于添加次數(shù)课锌,導(dǎo)致崩潰
*/
- (void)testKVOCrash12 {
// 崩潰日志:Cannot remove an observer XXX for the key path "xxx" from XXX because it is not registered as an observer.
[self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.objc.name = @"0";
[self.objc removeObserver:self forKeyPath:@"name"];
[self.objc removeObserver:self forKeyPath:@"name"];
}
/**
1.3 重復(fù)添加多次渺贤,雖然不會崩潰志鞍,但是發(fā)生改變時固棚,也同時會被觀察多次此洲。
*/
- (void)testKVOCrash13 {
[self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.objc.name = @"0";
}
/**
2. 被觀察者 dealloc 時仍然注冊著 KVO呜师,導(dǎo)致崩潰
*/
- (void)testKVOCrash2 {
// 崩潰日志:An instance xxx of class xxx was deallocated while key value observers were still registered with it.
// iOS 10 及以下會導(dǎo)致崩潰,iOS 11 之后就不會崩潰了
CrashObject *obj = [[CrashObject alloc] init];
[obj addObserver: self
forKeyPath: @"name"
options: NSKeyValueObservingOptionNew
context: nil];
obj = nil;
}
/**
3. 觀察者沒有實現(xiàn) -observeValueForKeyPath:ofObject:change:context:導(dǎo)致崩潰
*/
- (void)testKVOCrash3 {
// 崩潰日志:An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
CrashObject *obj = [[CrashObject alloc] init];
[self addObserver: obj
forKeyPath: @"title"
options: NSKeyValueObservingOptionNew
context: nil];
self.title = @"111";
}
/**
4. 添加或者移除時 keypath == nil碰酝,導(dǎo)致崩潰送爸。
*/
- (void)testKVOCrash4 {
// 崩潰日志: -[__NSCFConstantString characterAtIndex:]: Range or index out of bounds
CrashObject *obj = [[CrashObject alloc] init];
[self addObserver: obj
forKeyPath: @""
options: NSKeyValueObservingOptionNew
context: nil];
[self removeObserver:obj forKeyPath:@""];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context {
NSLog(@"object = %@, keyPath = %@", object, keyPath);
}
5.3 KVC Crash類型crash防護
? ?KVC(Key Value Coding),即鍵值編碼纹磺,提供一種機制來間接訪問對象的屬性橄杨。而不是通過調(diào)用Setter
式矫、Getter
方法進行訪問采转。
KVC 日常使用造成崩潰的原因通常有以下幾個:
??1. key 不是對象的屬性故慈,造成崩潰察绷。
??2. keyPath 不正確克婶,造成崩潰情萤。
??3. key 為 nil筋岛,造成崩潰睁宰。
??4. value 為 nil柒傻,為非對象設(shè)值红符,造成崩潰致开。
常見的使用 KVC 造成崩潰代碼:
/********************* CrashObject.h 文件 *********************/
#import <Foundation/Foundation.h>
@interface CrashObject : NSObject
@property (nonatomic, copy) NSString *name;
@end
/********************* ViewController.m 文件 *********************/
#import "ViewController.h"
#import "CrashObject.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 1. key 不是對象的屬性双戳,造成崩潰
// [self testKVCCrash1];
// 2. keyPath 不正確飒货,造成崩潰
// [self testKVCCrash2];
// 3. key 為 nil膏斤,造成崩潰
// [self testKVCCrash4];
// 4. value 為 nil傲茄,為非對象設(shè)值盘榨,造成崩潰
// [self testKVCCrash4];
}
/**
1. key 不是對象的屬性草巡,造成崩潰
*/
- (void)testKVCCrash1 {
// 崩潰日志:[<KVCCrashObject 0x600000d48ee0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key XXX.;
CrashObject *objc = [[CrashObject alloc] init];
[objc setValue:@"value" forKey:@"address"];
}
/**
2. keyPath 不正確山憨,造成崩潰
*/
- (void)testKVCCrash2 {
// 崩潰日志:[<KVCCrashObject 0x60000289afb0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key XXX.
CrashObject *objc = [[CrashObject alloc] init];
[objc setValue:@"后廠村路" forKeyPath:@"address.street"];
}
/**
3. key 為 nil,造成崩潰
*/
- (void)testKVCCrash3 {
// 崩潰日志:'-[KVCCrashObject setValue:forKey:]: attempt to set a value for a nil key
NSString *keyName;
// key 為 nil 會崩潰棚亩,如果傳 nil 會提示警告讥蟆,傳空變量則不會提示警告
CrashObject *objc = [[CrashObject alloc] init];
[objc setValue:@"value" forKey:keyName];
}
/**
4. value 為 nil瘸彤,造成崩潰
*/
- (void)testKVCCrash4 {
// 崩潰日志:[<KVCCrashObject 0x6000028a6780> setNilValueForKey]: could not set nil as the value for the key XXX.
// value 為 nil 會崩潰
CrashObject *objc = [[CrashObject alloc] init];
[objc setValue:nil forKey:@"name"];
}
@end
KVC的setter和getter方法
Setter方法
??系統(tǒng)在執(zhí)行setValue:forKey:
方法時低零,會把key和value作為輸入?yún)?shù)掏婶,并嘗試在接收調(diào)用對象的內(nèi)部雄妥,給屬性key設(shè)置value值。
通過以下幾個步驟:
`官方說明請查看 NSKeyValueCoding.h 文件`
1. 按順序查找名為 `set<Key>: `黎炉、`_set<Key>:`、 `setIs<Key>:`方法庆械。如果找到方法缭乘,則執(zhí)行該方法堕绩,使用輸入?yún)?shù)設(shè)置變量逛尚,則`setValue:forKey:`完成執(zhí)行刁愿。如果沒找到方法绰寞,則執(zhí)行下一步。
2. 訪問類的`accessInstanceVariablesDirectly`屬性铣口。如果 `accessInstanceVariablesDirectly`屬性返回**YES**滤钱,就按順序查找名為 `_<key>`、`_is<Key>`脑题、`<key>`、`is<Key>`的實例變量叔遂,如果找到了對應(yīng)的實例變量他炊,則使用輸入?yún)?shù)設(shè)置變量。則`setValue:forKey:`完成執(zhí)行已艰。如果未找到對應(yīng)的實例變量痊末,或者`accessInstanceVariablesDirectly`屬性返回**NO**則執(zhí)行下一步。
3. 調(diào)用`setValue: forUndefinedKey:`方法哩掺,并引發(fā)崩潰凿叠。
- 相關(guān)代碼:
CrashObject *objc = [[CrashObject alloc] init];
[objc setValue:@"value" forKey:@"name"];
Getter方法
??系統(tǒng)在執(zhí)行valueForKey:
方法時,會將給定的key作為輸入?yún)?shù),在調(diào)用對象的內(nèi)部進行以下幾個步驟:
??1. 按順序查找名為get<Key>
盒件、<key>
蹬碧、is<Key>
、_<key>
的訪問方法炒刁。如果找到恩沽,調(diào)用該方法,并繼續(xù)執(zhí)行步驟 5翔始。否則繼續(xù)向下執(zhí)行步驟 2飒筑。
??2. 搜索形如countOf<Key>
、objectIn<Key>AtIndex:
绽昏、<key>AtIndexes:
的方法协屡。
??3. 如果實現(xiàn)了countOf<Key>
方法,并且實現(xiàn)了objectIn<Key>AtIndex:
和<key>AtIndexes:
這兩個方法的任意一個方法全谤,系統(tǒng)就會以NSArray為父類肤晓,動態(tài)生成一個類型為NSKeyValueArray的集合類對象,并調(diào)用上邊的實現(xiàn)方法认然,將結(jié)果直接返回补憾。
??? - 如果對象還實現(xiàn)了形如get<Key>:range:
的方法,系統(tǒng)也會在必要的時候自動調(diào)用卷员。
??? - 如果上述操作不成功則繼續(xù)向下執(zhí)行步驟 3盈匾。
??? - 如果上邊兩步失敗,系統(tǒng)就會查找形如countOf<Key>
毕骡、 enumeratorOf<Key>
削饵、 memberOf<Key>:
的方法。系統(tǒng)會自動生成一個 NSSet類型的集合類對象未巫,該對象響應(yīng)所有NSSet方法并將結(jié)果返回窿撬。如果查找失敗,則執(zhí)行步驟 4叙凡。
??4. 如果上邊三步失敗劈伴,系統(tǒng)就會訪問類的accessInstanceVariablesDirectly
方法。
???? - 如果返回YES握爷,就按順序查找名為_<key>
跛璧、 _is<Key>
、 <key>
新啼、 is<Key>
的實例變量追城。如果找到了對應(yīng)的實例變量,則直接獲取實例變量的值师抄。并繼續(xù)執(zhí)行步驟 5漓柑。
???? - 如果返回NO,或者未找到對應(yīng)的實例變量叨吮,則繼續(xù)執(zhí)行步驟 6辆布。
??5. 分為三種情況:
??? - 如果檢索到的屬性值是對象指針,則直接返回結(jié)果茶鉴。
??? - 如果檢索到的屬性值是NSNumber支持的基礎(chǔ)數(shù)據(jù)類型锋玲,則將其存儲在 NSNumber 實例中并返回該值。
??? - 如果檢索到的屬性值是NSNumber不支持的數(shù)據(jù)類型涵叮,則轉(zhuǎn)換為 NSValue 對象并返回該對象惭蹂。
??6. 如果一切都失敗了,調(diào)用valueForUndefinedKey:
割粮,并引發(fā)崩潰盾碗。
KVC Crash 防護方案
??從 Setter 方法 和 Getter 方法 可以看出:
???? - setValue:forKey:
執(zhí)行失敗會調(diào)用setValue: forUndefinedKey:
方法,并引發(fā)崩潰舀瓢。
???? - valueForKey:
執(zhí)行失敗會調(diào)用valueForUndefinedKey:
方法廷雅,并引發(fā)崩潰。
??所以京髓,為了進行 KVC Crash 防護航缀,我們就需要重寫setValue: forUndefinedKey:
方法和valueForUndefinedKey:
方法。重寫這兩個方法之后堰怨,就可以防護1. key不是對象的屬性和2. keyPath不正確這兩種崩潰情況了芥玉。
??那么3. key為nil,造成崩潰的情況备图,該怎么防護呢灿巧?
??我們可以利用 Method Swizzling方法,在NSObject的分類中將 setValue:forKey:
和crashKiller_setValue:forKey:
進行方法交換揽涮。然后在自定義的方法中砸烦,添加對key為nil這種類型的判斷。
??還有最后一種4. value為nil绞吁,為非對象設(shè)值幢痘,造成崩潰 的情況。
??在NSKeyValueCoding.h
文件中家破,有一個setNilValueForKey:
方法颜说。上邊的官方注釋給了我們答案。
??在調(diào)用setValue:forKey:
方法時汰聋,系統(tǒng)如果查找到名為set<Key>:
方法的時候门粪,會去檢測value的參數(shù)類型,如果參數(shù)類型為NSNmber的標量類型或者是 NSValue的結(jié)構(gòu)類型烹困,但是value為nil時玄妈,會自動調(diào)用 setNilValueForKey:
方法。這個方法的默認實現(xiàn)會引發(fā)崩潰。
??所以為了防止這種情況導(dǎo)致的崩潰拟蜻,我們可以通過重寫setNilValueForKey:
來解決绎签。
??至此,上文提到的 KVC 使用不當造成的四種類型崩潰就都解決了酝锅。
下面我們來看下具體實現(xiàn)代碼:
+ (void)registerKillKVC
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[NSObject crashKillerMethodSwizzlingInstanceMethod:@selector(setValue:forKey:)
withMethod:@selector(crashKiller_setValue:forKey:)
withClass:[NSObject class]];
});
}
- (void)crashKiller_setValue:(id)value forKey:(NSString *)key {
@try {
[self crashKiller_setValue:value forKey:key];
} @catch (NSException *exception) {
[[CrashKillerManager shareManager] printLogWithException:exception];
}
}
- (void)setNilValueForKey:(NSString *)key {
NSString *reason = [NSString stringWithFormat:@"[<%@ %p> setValue:forUndefinedKey:]: could not set nil as the value for the key %@. ***",NSStringFromClass([self class]),self,key];
[[CrashKillerManager shareManager] throwExceptionWithName:@"NSUnknownKeyException" reason:reason];
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSString *reason = [NSString stringWithFormat:@"[<%@ %p> valueForUndefinedKey:]: this class is not key value coding-compliant for the key %@.",NSStringFromClass([self class]),self,key];
[[CrashKillerManager shareManager] throwExceptionWithName:@"NSUnknownKeyException" reason:reason];
}
- (nullable id)valueForUndefinedKey:(NSString *)key {
NSString *reason = [NSString stringWithFormat:@"[<%@ %p> valueForUndefinedKey:]: this class is not key value coding-compliant for the key %@.",NSStringFromClass([self class]),self,key];
[[CrashKillerManager shareManager] throwExceptionWithName:@"NSUnknownKeyException" reason:reason];
return self;
}
5.4 NSNotification Crash類型crash防護
? ?當一個對象添加了notification之后诡必,如果dealloc的時候,仍然持有notification搔扁,就會出現(xiàn)NSNotification類型的crash爸舒。
? ?NSNotification類型的crash多產(chǎn)生于程序員寫代碼時候犯疏忽,在NSNotificationCenter添加一個對象為observer之后稿蹲,忘記了在對象dealloc的時候移除它扭勉。
? ?所幸的是,蘋果在iOS9之后專門針對于這種情況做了處理苛聘,所以在iOS9之后涂炎,即使開發(fā)者沒有移除observer,Notification crash也不會再產(chǎn)生了焰盗。
5.5 NSTimer Crash類型crash防護
NSTimer crash 產(chǎn)生原因:
? ?在程序開發(fā)過程中璧尸,大家會經(jīng)常使用定時任務(wù),但使用NSTimer的 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
接口做重復(fù)性的定時任務(wù)時存在一個問題:NSTimer會強引用target實例熬拒,所以需要在合適的時機invalidate定時器爷光,否則就會由于定時器timer強引用target的關(guān)系導(dǎo)致target不能被釋放,造成內(nèi)存泄露澎粟,甚至在定時任務(wù)觸發(fā)時導(dǎo)致crash蛀序。 crash的展現(xiàn)形式和具體的target執(zhí)行的selector有關(guān)。
? ?與此同時活烙,如果NSTimer是無限重復(fù)的執(zhí)行一個任務(wù)的話徐裸,也有可能導(dǎo)致target的selector一直被重復(fù)調(diào)用且處于無效狀態(tài),對app的CPU啸盏,內(nèi)存等性能方面均是沒有必要的浪費重贺。
? ?所以,很有必要設(shè)計出一種方案回懦,可以有效的防護NSTimer的濫用問題气笙。
NSTimer crash 防護方案:
? ?上面的分析可見,NSTimer所產(chǎn)生的問題的主要原因是因為其沒有再一個合適的時機invalidate怯晕,同時還有NSTimer對target的強引用導(dǎo)致的內(nèi)存泄漏問題潜圃。
? ?那么解決NSTimer的問題的關(guān)鍵點在于以下兩點:
? ?? ?1. NSTimer對其target是否可以不強引用。
? ?? ?2. 是否找到一個合適的時機舟茶,在確定NSTimer已經(jīng)失效的情況下谭期,讓NSTimer自動invalidate堵第。
? ?關(guān)于第一個問題,target的強引用問題,可以用如下圖的方案來解決:
? ?在NSTimer和target之間加入一層stubTarget隧出,stubTarget主要做為一個橋接層踏志,負責(zé)NSTimer和target之間的通信。同時NSTimer強引用stubTarget鸳劳,而stubTarget弱引用target狰贯,這樣target和NSTimer之間的關(guān)系也就是弱引用了也搓,意味著target可以自由的釋放赏廓,從而解決了循環(huán)引用的問題。
? ?上文提到了stubTarget負責(zé)NSTimer和target的通信傍妒,其具體的實現(xiàn)過程又細分為兩大步:
? ?1. swizzle NSTimer中scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
相關(guān)的方法幔摸,在新方法中動態(tài)創(chuàng)建stubTarget對象,stubTarget對象弱引用持有原有的target颤练,selector既忆,timer,targetClass等properties嗦玖。然后將原target分發(fā)stubTarget上患雇,selector回調(diào)函數(shù)為stubTarget的fireProxyTimer;
? ?2. 通過stubTarget的fireProxyTimer:來具體處理回調(diào)函數(shù)selector的處理和分發(fā),當NSTimer的回調(diào)函數(shù)fireProxyTimer:被執(zhí)行的時候宇挫,會自動判斷原target是否已經(jīng)被釋放苛吱,如果釋放了,意味著NSTimer已經(jīng)無效器瘪,此時如果還繼續(xù)調(diào)用原有target的selector很有可能會導(dǎo)致crash翠储,而且是沒有必要的。所以此時需要將NSTimer invalidate橡疼。
? ?如此一來就做到了NSTimer在合適的時機自動invalidate援所。
5.6 Container / NSString 類型crash防護
? ?這個問題比較簡單,就是對一些常用類中會導(dǎo)致崩潰的API進行method swizzling,然后在swizzle的新方法中加入一些條件限制和判斷欣除,從而讓這些API變的安全住拭。
? ?目前對以下類進行防范,類中可能導(dǎo)致crash的方法历帚,逐步進行增量擴充滔岳。
? ?NSArray/NSMutableArray
? ?NSDictionary/NSMutableDictionary
? ?NSString / NSMutableString
5.7 非主線程刷UI類型crash防護(UI not on Main Thread)
初步處理方案就是swizzle UIView類的以下三個方法:
- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;
在這三個方法調(diào)用的時候判斷 一下當前的線程,如果不是主線程的話抹缕,直接利用dispatch_async(dispatch_get_main_queue(), ^{ //調(diào)用原本方法 });
來將對應(yīng)的刷新UI的操作轉(zhuǎn)移到主線程澈蟆。
考慮到此三個方法調(diào)用頻率過高,且在開發(fā)階段xcode就會給出提示卓研;故不考慮維護此類異常crah趴俘。