iOS你不知道的事--Crash分析

大家平時(shí)在開發(fā)過(guò)程中,經(jīng)常會(huì)遇到Crash,那也是在正常不過(guò)的事,但是作為一個(gè)優(yōu)秀的iOS開發(fā)人員,必將這些用戶不良體驗(yàn)降到最低。

  • 線下Crash,我們直接可以調(diào)試,結(jié)合stack信息,不難定位!
  • 線上Crash當(dāng)然也有一些信息,畢竟蘋果爸爸的產(chǎn)品還是做得非常不錯(cuò)的!

通過(guò)iPhone的Crash log也可以分析一些,但是這個(gè)是需要用戶配合的,因?yàn)樾枰脩粼谑謾C(jī) 中 設(shè)置-> 診斷與用量->勾選 自動(dòng)發(fā)送 ,然后在xcode中 Window->Organizer->Crashes 對(duì)應(yīng)的app,就是當(dāng)前app最新一版本的crash log ,并且是解析過(guò)的,可以根據(jù)crash 棧 等相關(guān)信息 ,尤其是程序代碼級(jí)別的 有超鏈接,一鍵可以直接跳轉(zhuǎn)到程序崩潰的相關(guān)代碼,這樣更容易定位bug出處.

為了能夠第一時(shí)間發(fā)現(xiàn)程序問(wèn)題,應(yīng)用程序需要實(shí)現(xiàn)自己的崩潰日志收集服務(wù),成熟的開源項(xiàng)目很多,如 KSCrash,plcrashreporter,CrashKit 等。追求方便省心,對(duì)于保密性要求不高的程序來(lái)說(shuō),也可以選擇各種一條龍Crash統(tǒng)計(jì)產(chǎn)品,如 CrashlyticsHockeyapp ,友盟Bugly 等等

但是,所有的但是,這不夠!因?yàn)槲覀儾辉偈且粋€(gè)簡(jiǎn)單會(huì)用的iOS開發(fā)人員,必將走向底層,了解原理,掌握裝逼內(nèi)容和技巧是我們的必修課

首先我們來(lái)了解一下Crash的底層原理

iOS系統(tǒng)自帶的 Apple’s Crash Reporter記錄在設(shè)備中的Crash日志,Exception Type項(xiàng)通常會(huì)包含兩個(gè)元素:Mach異常Unix信號(hào)。

Exception Type:         EXC_BAD_ACCESS (SIGSEGV)    
Exception Subtype:      KERN_INVALID_ADDRESS at 0x041a6f3

Mach異常是什么?它又是如何與Unix信號(hào)建立聯(lián)系的?

Mach是一個(gè)XNU的微內(nèi)核核心,Mach異常是指最底層的內(nèi)核級(jí)異常,被定義在下 。每個(gè)thread,task,host都有一個(gè)異常端口數(shù)組,Mach的部分API暴露給了用戶態(tài),用戶態(tài)的開發(fā)者可以直接通過(guò)Mach API設(shè)置thread,task,host的異常端口,來(lái)捕獲Mach異常,抓取Crash事件。

所有Mach異常都在host層被ux_exception轉(zhuǎn)換為相應(yīng)的Unix信號(hào),并通過(guò)threadsignal將信號(hào)投遞到出錯(cuò)的線程。iOS中的 POSIX API就是通過(guò)Mach之上的 BSD層實(shí)現(xiàn)的。

因此,EXC_BAD_ACCESS (SIGSEGV)表示的意思是:Mach層的EXC_BAD_ACCESS異常,在host層被轉(zhuǎn)換成SIGSEGV信號(hào)投遞到出錯(cuò)的線程。

iOS的異常Crash

* KVO問(wèn)題
* NSNotification線程問(wèn)題
* 數(shù)組越界
* 野指針
* 后臺(tái)任務(wù)超時(shí)
* 內(nèi)存爆出
* 主線程卡頓超閥值
* 死鎖
....

下面我就拿出最常見(jiàn)的兩種Crash分析一下

Exception

Signal

Crash分析處理

上面我們也知道:既然最終以信號(hào)的方式投遞到出錯(cuò)的線程,那么就可以通過(guò)注冊(cè)相應(yīng)函數(shù)來(lái)捕獲信號(hào).到達(dá)Hook的效果

+ (void)installUncaughtSignalExceptionHandler{
    NSSetUncaughtExceptionHandler(&LGExceptionHandlers);
    signal(SIGABRT, LGSignalHandler);
}

關(guān)于Signal參考

我們從上面的函數(shù)可以Hook到信息,下面我們開始進(jìn)行包裝處理.這里還是面向統(tǒng)一封裝,因?yàn)榈葧?huì)我們還需要考慮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];
}

下面針對(duì)封裝好的myException進(jìn)行處理,在這里要做兩件事

  • 存儲(chǔ),上傳:方便開發(fā)人員檢查修復(fù)
  • 處理Crash奔潰,我們也不能眼睜睜看著BUG閃退在用戶的手機(jī)上面,希望“起死回生,回光返照”
- (void)lg_handleException:(NSException *)exception{
    // crash 處理
    // 存
    NSDictionary *userInfo = [exception userInfo];
    [self saveCrash:exception file:[userInfo objectForKey:LGUncaughtExceptionHandlerFileKey]];
}

下面是一些封裝的一些輔助函數(shù)

  • 保存奔潰信息或者上傳:針對(duì)封裝數(shù)據(jù)本地存儲(chǔ),和相應(yīng)上傳服務(wù)器!
- (void)saveCrash:(NSException *)exception file:(NSString *)file{
    
    NSArray *stackArray = [[exception userInfo] objectForKey:LGUncaughtExceptionHandlerCallStackSymbolsKey];// 異常的堆棧信息
    NSString *reason = [exception reason];// 出現(xiàn)異常的原因
    NSString *name = [exception name];// 異常名稱
    
    // 或者直接用代碼,輸入這個(gè)崩潰信息,以便在console中進(jìn)一步分析錯(cuò)誤原因
    // 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ù)堆棧信息,這里可以獲取響應(yīng)調(diào)用堆棧的符號(hào)信息,通過(guò)數(shù)組回傳
+ (NSArray *)lg_backtrace{
    
    void* callstack[128];
    int frames = backtrace(callstack, 128);//用于獲取當(dāng)前線程的函數(shù)調(diào)用堆棧,返回實(shí)際獲取的指針個(gè)數(shù)
    char **strs = backtrace_symbols(callstack, frames);//從backtrace函數(shù)獲取的信息轉(zhuǎn)化為一個(gè)字符串?dāng)?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;
}

  • 獲取應(yīng)用信息,這個(gè)函數(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;
}

做完這些準(zhǔn)備,你可以非常清晰的看到程序奔潰,哈哈哈?。ê孟褚郧氨紳⑦€不清晰似的),這里說(shuō)一下:我的意思你非常清晰的知道奔潰之前做了一些什么!
下面是檢測(cè)我們奔潰之前的沙盒存儲(chǔ)的信息:error.log

下面我們來(lái)一個(gè)騷操作:在監(jiān)聽(tīng)的信息的時(shí)候來(lái)了一個(gè)Runloop,我們監(jiān)聽(tīng)所有的mode,開啟循環(huán)(一個(gè)相對(duì)于我們應(yīng)用程序自啟的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
    // 后臺(tái)更新 - log
    // kill
    // 
    for (NSString *mode in (__bridge NSArray *)allMode) {
        CFRunLoopRunInMode((CFStringRef)mode, 0.0001, false);
    }
}

CFRelease(allMode);

在這個(gè)平行空間我們開啟一個(gè)彈框,這個(gè)彈框,跟著我們的應(yīng)用程序?;?,并且具備相應(yīng)的響應(yīng)能力,到目前為止:此時(shí)此刻還有誰(shuí)!這不就是回光返照?只要我們的條件成立,那么在相應(yīng)的這個(gè)平行空間繼續(xù)做一些我們的工作,程序不死:what is dead may never die,but rises again harder and stronger

signal 函數(shù)攔截不到的解決方式

在debug模式下,如果你觸發(fā)了崩潰,那么應(yīng)用會(huì)直接崩潰到主函數(shù),斷點(diǎn)都沒(méi)用,此時(shí)沒(méi)有任何log信息顯示出來(lái),如果你想看log信息的話,你需要在lldb中,拿SIGABRT來(lái)說(shuō)吧,敲入pro hand -p true -s false SIGABRT命令,不然你啥也看不到。

然后斷開斷點(diǎn),程序進(jìn)入監(jiān)聽(tīng),下面剩下的操作就是包裝異常,操作類似Exception

最后我們需要注意的針對(duì)我們的監(jiān)聽(tīng)回收相應(yīng)內(nèi)存:

   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];
    }

到目前為止,我們響應(yīng)的Crash處理已經(jīng)入門,如果你還想繼續(xù)探索也是有很多地方比如:

  • 我們能否hook系統(tǒng)奔潰,異常的方法NSSetUncaughtExceptionHandler,已達(dá)到拒絕傳遞 UncaughtExceptionHandler的效果
  • 我們?cè)谔幚懋惓5臅r(shí)候,利用Runloop回光返照,有沒(méi)有更加合適的方法
  • Runloop回光返照我們?cè)趺蠢^續(xù)保證應(yīng)用程序穩(wěn)定執(zhí)行

如果你們有相應(yīng)比較好的方式方法都可以直接留言,或者微信聯(lián)系我

我就是我,顏色不一樣的煙火,我是Cooci,和諧學(xué)習(xí),不急不躁!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容