大家平時在開發(fā)過程中种呐,經(jīng)常會遇到
Crash
,那也是在正常不過的事弃甥,但是作為一個優(yōu)秀的iOS開發(fā)人員爽室,必將這些用戶不良體驗降到最低。
- 線下
Crash
,我們直接可以調試淆攻,結合stack
信息肮之,不難定位! - 線上
Crash
當然也有一些信息卜录,畢竟蘋果爸爸的產(chǎn)品還是做得非常不錯的戈擒!
通過iPhone的Crash log
也可以分析一些,但是這個是需要用戶配合的,因為需要用戶在手機 中 設置-> 診斷與用量->勾選 自動發(fā)送 ,然后在xcode中 Window->Organizer->Crashes 對應的app
,就是當前app最新一版本的crash log
,并且是解析過的,可以根據(jù)crash 棧
等相關信息 ,尤其是程序代碼級別的 有超鏈接,一鍵可以直接跳轉到程序崩潰的相關代碼,這樣更容易定位bug出處.
為了能夠第一時間發(fā)現(xiàn)程序問題艰毒,應用程序需要實現(xiàn)自己的崩潰日志收集服務筐高,成熟的開源項目很多,如 KSCrash,plcrashreporter柑土,CrashKit 等蜀肘。追求方便省心,對于保密性要求不高的程序來說稽屏,也可以選擇各種一條龍Crash統(tǒng)計產(chǎn)品扮宠,如 Crashlytics,Hockeyapp 狐榔,友盟坛增,Bugly 等等
但是,所有的但是薄腻,這不夠收捣!因為我們不再是一個簡單會用的iOS開發(fā)人員,必將走向底層庵楷,了解原理罢艾,掌握裝逼內容和技巧是我們的必修課
首先我們來了解一下Crash
的底層原理
iOS系統(tǒng)自帶的 Apple’s Crash Reporter
記錄在設備中的Crash日志,Exception Type
項通常會包含兩個元素:Mach異常
和 Unix信號
尽纽。
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x041a6f3
Mach異常是什么咐蚯?它又是如何與Unix信號建立聯(lián)系的?
Mach
是一個XNU的微內核核心弄贿,Mach
異常是指最底層的內核級異常仓蛆,被定義在下 。每個thread挎春,task看疙,host
都有一個異常端口數(shù)組,Mach
的部分API
暴露給了用戶態(tài)直奋,用戶態(tài)的開發(fā)者可以直接通過Mach API
設置thread能庆,task,host
的異常端口脚线,來捕獲Mach
異常搁胆,抓取Crash
事件。
所有Mach
異常都在host
層被ux_exception
轉換為相應的Unix信號
邮绿,并通過threadsignal
將信號投遞到出錯的線程渠旁。iOS中的 POSIX API
就是通過Mach
之上的 BSD
層實現(xiàn)的。
因此船逮,EXC_BAD_ACCESS (SIGSEGV)
表示的意思是:Mach
層的EXC_BAD_ACCESS異常
顾腊,在host
層被轉換成SIGSEGV信號
投遞到出錯的線程。
iOS的異常Crash
* KVO問題
* NSNotification線程問題
* 數(shù)組越界
* 野指針
* 后臺任務超時
* 內存爆出
* 主線程卡頓超閥值
* 死鎖
....
下面我就拿出最常見的兩種Crash
分析一下
Exception
Signal
Crash
分析處理
上面我們也知道:既然最終以信號的方式投遞到出錯的線程挖胃,那么就可以通過注冊相應函數(shù)來捕獲信號.到達Hook
的效果
+ (void)installUncaughtSignalExceptionHandler{
NSSetUncaughtExceptionHandler(&LGExceptionHandlers);
signal(SIGABRT, LGSignalHandler);
}
我們從上面的函數(shù)可以Hook到信息杂靶,下面我們開始進行包裝處理.這里還是面向統(tǒng)一封裝梆惯,因為等會我們還需要考慮Signal
void LGExceptionHandlers(NSException *exception) {
NSLog(@"%s",__func__);
NSArray *callStack = [LGUncaughtExceptionHandle lg_backtrace];
NSMutableDictionary *mDict = [NSMutableDictionary dictionaryWithDictionary:exception.userInfo];
[mDict setObject:callStack forKey:LGUncaughtExceptionHandlerAddressesKey];
[mDict setObject:exception.callStackSymbols forKey:LGUncaughtExceptionHandlerCallStackSymbolsKey];
[mDict setObject:@"LGException" forKey:LGUncaughtExceptionHandlerFileKey];
// exception - myException
[[[LGUncaughtExceptionHandle alloc] init] performSelectorOnMainThread:@selector(lg_handleException:) withObject:[NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:mDict] waitUntilDone:YES];
}
下面針對封裝好的myException
進行處理,在這里要做兩件事
- 存儲,上傳:方便開發(fā)人員檢查修復
- 處理Crash奔潰吗垮,我們也不能眼睜睜看著
BUG
閃退在用戶的手機上面垛吗,希望“起死回生,回光返照”
- (void)lg_handleException:(NSException *)exception{
// crash 處理
// 存
NSDictionary *userInfo = [exception userInfo];
[self saveCrash:exception file:[userInfo objectForKey:LGUncaughtExceptionHandlerFileKey]];
}
下面是一些封裝的一些輔助函數(shù)
- 保存奔潰信息或者上傳:針對封裝數(shù)據(jù)本地存儲烁登,和相應上傳服務器怯屉!
- (void)saveCrash:(NSException *)exception file:(NSString *)file{
NSArray *stackArray = [[exception userInfo] objectForKey:LGUncaughtExceptionHandlerCallStackSymbolsKey];// 異常的堆棧信息
NSString *reason = [exception reason];// 出現(xiàn)異常的原因
NSString *name = [exception name];// 異常名稱
// 或者直接用代碼,輸入這個崩潰信息饵沧,以便在console中進一步分析錯誤原因
// NSLog(@"crash: %@", exception);
NSString * _libPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:file];
if (![[NSFileManager defaultManager] fileExistsAtPath:_libPath]){
[[NSFileManager defaultManager] createDirectoryAtPath:_libPath withIntermediateDirectories:YES attributes:nil error:nil];
}
NSDate *dat = [NSDate dateWithTimeIntervalSinceNow:0];
NSTimeInterval a=[dat timeIntervalSince1970];
NSString *timeString = [NSString stringWithFormat:@"%f", a];
NSString * savePath = [_libPath stringByAppendingFormat:@"/error%@.log",timeString];
NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
BOOL sucess = [exceptionInfo writeToFile:savePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
NSLog(@"保存崩潰日志 sucess:%d,%@",sucess,savePath);
}
- 獲取函數(shù)堆棧信息锨络,這里可以獲取響應調用堆棧的符號信息,通過數(shù)組回傳
+ (NSArray *)lg_backtrace{
void* callstack[128];
int frames = backtrace(callstack, 128);//用于獲取當前線程的函數(shù)調用堆棧捷泞,返回實際獲取的指針個數(shù)
char **strs = backtrace_symbols(callstack, frames);//從backtrace函數(shù)獲取的信息轉化為一個字符串數(shù)組
int i;
NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
for (i = LGUncaughtExceptionHandlerSkipAddressCount;
i < LGUncaughtExceptionHandlerSkipAddressCount+LGUncaughtExceptionHandlerReportAddressCount;
i++)
{
[backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
return backtrace;
}
- 獲取應用信息足删,這個函數(shù)提供給
Siganl
數(shù)據(jù)封裝
NSString *getAppInfo(){
NSString *appInfo = [NSString stringWithFormat:@"App : %@ %@(%@)\nDevice : %@\nOS Version : %@ %@\n",
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"],
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"],
[UIDevice currentDevice].model,
[UIDevice currentDevice].systemName,
[UIDevice currentDevice].systemVersion];
// [UIDevice currentDevice].uniqueIdentifier];
NSLog(@"Crash!!!! %@", appInfo);
return appInfo;
}
做完這些準備寿谴,你可以非常清晰的看到程序奔潰锁右,哈哈哈!(好像以前奔潰還不清晰似的)讶泰,這里說一下:我的意思你非常清晰的知道奔潰之前做了一些什么!
下面是檢測我們奔潰之前的沙盒存儲的信息:error.log
下面我們來一個騷操作:在監(jiān)聽的信息的時候來了一個Runloop
,我們監(jiān)聽所有的mode
,開啟循環(huán)(一個相對于我們應用程序自啟的Runloop
的平行空間).
SCLAlertView *alert = [[SCLAlertView alloc] initWithNewWindowWidth:300.0f];
[alert addButton:@"奔潰" actionBlock:^{
self.dismissed = YES;
}];
[alert showSuccess:exception.name subTitle:exception.reason closeButtonTitle:nil duration:0];
// 本次異常處理
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFArrayRef allMode = CFRunLoopCopyAllModes(runloop);
while (!self.dismissed) {
// machO
// 后臺更新 - log
// kill
//
for (NSString *mode in (__bridge NSArray *)allMode) {
CFRunLoopRunInMode((CFStringRef)mode, 0.0001, false);
}
}
CFRelease(allMode);
在這個平行空間
我們開啟一個彈框咏瑟,這個彈框,跟著我們的應用程序被臼穑活码泞,并且具備相應的響應能力,到目前為止:此時此刻還有誰狼犯!這不就是回光返照
余寥?只要我們的條件成立,那么在相應的這個平行空間
繼續(xù)做一些我們的工作悯森,程序不死:what is dead may never die,but rises again harder and stronger
signal 函數(shù)攔截不到的解決方式
在debug模式下宋舷,如果你觸發(fā)了崩潰,那么應用會直接崩潰到主函數(shù)瓢姻,斷點都沒用祝蝠,此時沒有任何log信息顯示出來,如果你想看log信息的話幻碱,你需要在lldb
中绎狭,拿SIGABRT
來說吧,敲入pro hand -p true -s false SIGABRT
命令褥傍,不然你啥也看不到儡嘶。
然后斷開斷點,程序進入監(jiān)聽恍风,下面剩下的操作就是包裝異常社付,操作類似Exception
最后我們需要注意的針對我們的監(jiān)聽回收相應內存:
NSSetUncaughtExceptionHandler(NULL);
signal(SIGABRT, SIG_DFL);
signal(SIGILL, SIG_DFL);
signal(SIGSEGV, SIG_DFL);
signal(SIGFPE, SIG_DFL);
signal(SIGBUS, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
if ([[exception name] isEqual:UncaughtExceptionHandlerSignalExceptionName])
{
kill(getpid(), [[[exception userInfo] objectForKey:UncaughtExceptionHandlerSignalKey] intValue]);
}
else
{
[exception raise];
}
到目前為止承疲,我們響應的Crash
處理已經(jīng)入門,如果你還想繼續(xù)探索也是有很多地方比如:
- 我們能否hook系統(tǒng)奔潰鸥咖,異常的方法
NSSetUncaughtExceptionHandler
,已達到拒絕傳遞UncaughtExceptionHandler
的效果 - 我們在處理異常的時候燕鸽,利用
Runloop回光返照
,有沒有更加合適的方法 -
Runloop回光返照
我們怎么繼續(xù)保證應用程序穩(wěn)定執(zhí)行
如果你們有相應比較好的方式方法都可以直接留言啼辣,或者微信聯(lián)系我
我就是我啊研,顏色不一樣的煙火,我是Cooci鸥拧,和諧學習党远,不急不躁!