用戶在使用App的過程中,經(jīng)常遇到閃退的情況,體驗(yàn)不太好,本文嘗試探索引發(fā)閃退的原因,以及在遇到crash的情況下,盡可能的保持程序運(yùn)行,并及時(shí)上報(bào)錯(cuò)誤。
一、crash類型
1.OC層面的crash
1.1 普通類型
- NSInvalidArgumentException:非法參數(shù)異常,傳入非法參數(shù)導(dǎo)致異常,nil參數(shù)比較常見。
- NSRangeException:下標(biāo)越界導(dǎo)致的異常。
- NSGenericException: foreach的循環(huán)當(dāng)中修改元素導(dǎo)致的異常。
1.2 KVO
KVO Crash常見原因:
- 移除未注冊的觀察者
- 重復(fù)移除觀察者
- 添加了觀察者但是沒有實(shí)現(xiàn)
-observeValueForKeyPath:ofObject:change:context:方法 - 添加移除keypath=nil
- 添加移除observer=nil
1.3 unrecognized selector sent to instance
-
對象接收到未知的消息,即下圖中消息未能處理的情況。
image<figcaption></figcaption>
2.Signal層面的crash
除了OC層面的異常捕獲之外,很多內(nèi)存錯(cuò)誤、訪問錯(cuò)誤的地址產(chǎn)生的crash則需要利用unix標(biāo)準(zhǔn)的signal機(jī)制,注冊SIGABRT, SIGBUS, SIGSEGV等信號發(fā)生時(shí)的處理函數(shù)。該函數(shù)中我們可以輸出棧信息,版本信息等其他一切我們所想要的。
- SIGKILL:用來立即結(jié)束程序的運(yùn)行的信號。
- SIGSEGV:試圖訪問未分配給自己的內(nèi)存, 或試圖往沒有寫權(quán)限的內(nèi)存地址寫數(shù)據(jù)。
- SIGABRT:調(diào)用abort函數(shù)生成的信號。
- SIGTRAP:由斷點(diǎn)指令或其它trap指令產(chǎn)生。
- SIGBUS:非法地址, 包括內(nèi)存地址對齊(alignment)出錯(cuò)。比如訪問一個(gè)四個(gè)字長的整數(shù), 但其地址不是4的倍數(shù)。它與SIGSEGV的區(qū)別在于后者是由于對合法存儲(chǔ)地址的非法訪問觸發(fā)的(如訪問不屬于自己存儲(chǔ)空間或只讀存儲(chǔ)空間)。
二、存在問題
- 程序閃退,用戶體驗(yàn)不好
三、監(jiān)聽crash
1.任憑程序閃退并上報(bào)
1.1 NSSetUncaughtExceptionHandler 捕獲OC層面的crash
(1)AppDelegate中添加捕獲監(jiān)聽
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
return YES;
}
復(fù)制代碼
(2)解析堆棧信息并上報(bào)
void UncaughtExceptionHandler(NSException *exception) {
/**
* 獲取異常崩潰信息
*/
NSArray *callStack = [exception callStackSymbols];
NSString *reason = [exception reason];
NSString *name = [exception name];
}
復(fù)制代碼
1.2 Appdelegate中注冊SIGABRT, SIGBUS, SIGSEGV等信號發(fā)生時(shí)的處理函數(shù),處理Signal層面的crash。
void InstallSignalHandler(void)
{
signal(SIGHUP, SignalExceptionHandler);
signal(SIGINT, SignalExceptionHandler);
signal(SIGQUIT, SignalExceptionHandler);
signal(SIGABRT, SignalExceptionHandler);
signal(SIGILL, SignalExceptionHandler);
signal(SIGSEGV, SignalExceptionHandler);
signal(SIGFPE, SignalExceptionHandler);
signal(SIGBUS, SignalExceptionHandler);
signal(SIGPIPE, SignalExceptionHandler);
}
復(fù)制代碼
void SignalExceptionHandler(int signal)
{
NSMutableString *mstr = [[NSMutableString alloc] init];
[mstr appendString:@"Stack:\n"];
void* callstack[128];
int i, frames = backtrace(callstack, 128);
char** strs = backtrace_symbols(callstack, frames);
for (i = 0; i <frames; ++i) {
[mstr appendFormat:@"%s\n", strs[I]];
}
[SignalHandler saveCreash:mstr];
}
復(fù)制代碼
2.Crash自動(dòng)修復(fù)+捕獲上報(bào)
2.1 針對普通類型Crash的處理機(jī)制
hook相關(guān)的方法,增加保護(hù)機(jī)制。 以NSArray越界為例,hook objectAtIndex方法,在方法中捕獲越界異常,并在最后返回一個(gè)nil對象。
[self exchangeInstanceMethod:__NSArrayI method1Sel:@selector(objectAtIndex:) method2Sel:@selector(avoidCrashObjectAtIndex:)];
復(fù)制代碼
- (id)avoidCrashObjectAtIndex:(NSUInteger)index {
id object = nil;
@try {
object = [self avoidCrashObjectAtIndex:index];
}
@catch (NSException *exception) {
//捕獲異常,根據(jù)exception打印出堆棧信息,同時(shí)也避免了程序崩潰
}
@finally {
return object;
}
}
復(fù)制代碼
注意:使用方法進(jìn)行捕獲異常之后,第三方工具將不會(huì)搜集到崩潰信息并上報(bào),需要在catch中手動(dòng)上報(bào)。
2.2 針對KVO Crash的處理機(jī)制
新建一個(gè)對象,用來記錄target,observer,context,keypath等,每添加一個(gè)監(jiān)聽,增加一個(gè)對象,用一個(gè)數(shù)組維護(hù)。添加和刪除的時(shí)候做判斷,同時(shí)hook dealloc函數(shù),dealloc的同時(shí)移除我的觀察者和我觀察的對象。dealloc時(shí)遍歷數(shù)組,數(shù)組中不應(yīng)該存在對象,如果存在對象,應(yīng)該拋出異常并接收,提示用戶KVO的釋放存在問題。
- 移除未注冊的觀察者:在移除A對象的觀察者時(shí),先判斷數(shù)組中是否有A對象的觀察者,如果有,再移除。
- 重復(fù)移除觀察者:同上
- 添加了觀察者但是沒有實(shí)現(xiàn)
-observeValueForKeyPath:ofObject:change:context:方法:hookobserveValueForKeyPath方法,增加try-catch即可。 - 添加移除keypath=nil:hook添加移除觀察者的方法,在新方法中過濾keypath=nil的情況。
- 添加移除observer=nil:hook添加移除觀察者的方法,在新方法中過濾observer=nil的情況。
注意:使用方法進(jìn)行捕獲異常之后,第三方工具將不會(huì)搜集到崩潰信息并上報(bào),需要在catch中手動(dòng)上報(bào)。
2.3 針對unrecognized selector解決方案
通常,當(dāng)我們不能確定一個(gè)對象是否能接收某個(gè)消息時(shí),會(huì)先調(diào)用respondsToSelector:來判斷一下。如下代碼所示:
if ([self respondsToSelector:@selector(method)]) {
[self performSelector:@selector(method)];
}
復(fù)制代碼
當(dāng)一個(gè)對象無法接收某一消息時(shí),就會(huì)啟動(dòng)所謂”消息轉(zhuǎn)發(fā)(message forwarding)“機(jī)制,通過這一機(jī)制,我們可以告訴對象如何處理未知的消息。默認(rèn)情況下,對象接收到未知的消息,會(huì)導(dǎo)致程序崩潰。
<figcaption></figcaption>
上圖可以看出,在一個(gè)函數(shù)找不到時(shí),Objective-C提供了三種方式去補(bǔ)救:
1、調(diào)用resolveInstanceMethod給個(gè)機(jī)會(huì)讓類添加這個(gè)實(shí)現(xiàn)這個(gè)函數(shù)
2、調(diào)用forwardingTargetForSelector讓別的對象去執(zhí)行這個(gè)函數(shù)
3、調(diào)用methodSignatureForSelector(函數(shù)符號制造器)和forwardInvocation(函數(shù)執(zhí)行器)靈活的將目標(biāo)函數(shù)以其他形式執(zhí)行。
如果都不中,調(diào)用doesNotRecognizeSelector拋出異常。
- (void)forwardInvocation:(NSInvocation *)anInvocation
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
方法一:hook上述兩個(gè)方法,在methodSignatureForSelector中返回有效的NSMethodSignature,在forwardInvocation中添加try-catch即可,代碼如下:
[self exchangeInstanceMethod:[self class] method1Sel:@selector(methodSignatureForSelector:) method2Sel:@selector(avoidCrashMethodSignatureForSelector:)];
[self exchangeInstanceMethod:[self class] method1Sel:@selector(forwardInvocation:) method2Sel:@selector(avoidCrashForwardInvocation:)];
復(fù)制代碼
- (NSMethodSignature *)avoidCrashMethodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *ms = [self avoidCrashMethodSignatureForSelector:aSelector];
if ([self respondsToSelector:aSelector] || ms){
return ms;
}
else{
return [SafeProxy instanceMethodSignatureForSelector:@selector(safe_crashLog)];
}
}
- (void)avoidCrashForwardInvocation:(NSInvocation *)anInvocation {
@try {
[self avoidCrashForwardInvocation:anInvocation];
} @catch (NSException *exception) {
//捕獲異常,根據(jù)exception打印出堆棧信息,同時(shí)也避免了程序崩潰
//上報(bào)
} @finally {
}
}
復(fù)制代碼
方法二:直接hook doesNotRecognizeSelector也可實(shí)現(xiàn),doesNotRecognizeSelector起到拋出異常的作用,自己增加try-catch進(jìn)行捕獲即可,代碼如下:
[self exchangeInstanceMethod:[self class] method1Sel:@selector(doesNotRecognizeSelector:) method2Sel:@selector(avoidCrashDoesNotRecognizeSelector:)];
復(fù)制代碼
- (void)avoidCrashDoesNotRecognizeSelector:(SEL)aSelector{
@try {
[self avoidCrashDoesNotRecognizeSelector:aSelector];
} @catch (NSException *exception) {
//捕獲異常,根據(jù)exception打印出堆棧信息,同時(shí)也避免了程序崩潰
//上報(bào)
} @finally {
}
}
復(fù)制代碼
效果如下:
NSInvalidArgumentException
*** -[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[1]
Error Place:-[ViewController NSArray_Test_InstanceArray]
AvoidCrash default is to remove nil object and instance a array.
復(fù)制代碼
打印出了堆棧信息,同時(shí)避免了程序崩潰。
注意:使用方法進(jìn)行捕獲異常之后,第三方工具將不會(huì)搜集到崩潰信息并上報(bào),需要在catch中手動(dòng)上報(bào)。
2.4 針對野指針的處理機(jī)制
模仿Xcode的zombie機(jī)制:
1.Swizzle原有allocWithZone方法,添加野指針防護(hù)標(biāo)記。
2.Swizzle原有dealloc方法,如果有野指針防護(hù)標(biāo)記,調(diào)用 objc_destructInstance方法,修改實(shí)例isa使其指向zombieObject,保存原始 類名,以便上報(bào)使用。
3.Swizzle消息轉(zhuǎn)發(fā)機(jī)制forwardingTargetForSelector方法,處理所 有原始類originObject的方法,收集錯(cuò)誤信息并上報(bào)。
4.及時(shí)釋放zombieObject。
注: objc_destructInstance會(huì)釋放與實(shí)例相關(guān)聯(lián)的引用,但是并不釋放該實(shí)例的內(nèi)存。
作者:乳豬嘯谷
鏈接:https://juejin.im/post/6844903688608153614