iOS 崩潰信息收集實(shí)踐

iOS 崩潰信息收集

最近項(xiàng)目要求收集應(yīng)用使用過程中的崩潰信息,在網(wǎng)上搜索了一番后,了解目前崩潰信息收集有如下幾種途徑:iTunes Connect導(dǎo)出手機(jī)上傳日志、拿到用戶手機(jī)使用 Xcode 導(dǎo)出、使用第三方崩潰收集服務(wù)(如 Bugly、友盟等)。從及時(shí)性和可定制角度來看上面幾種都不符合項(xiàng)目的需求,基于上述需求背景要求必須學(xué)習(xí)手動(dòng)收集崩潰信息。

導(dǎo)致崩潰的問題

導(dǎo)致應(yīng)用崩潰的問題主要有兩種:

  1. C++語言層面的錯(cuò)誤,比如野指針、除零、內(nèi)存非法訪問等;
  2. 未捕獲異常(Uncaught Exception),在 iOS 中最常見的就是通過 @throw 拋出的 NSException(常見的錯(cuò)誤,比如數(shù)組訪問越界)

對(duì)于第一種問題,由于 iOS 和 Android 底層系統(tǒng)都是 Unix 或者類 Unix 系統(tǒng),可以采用信號(hào)機(jī)制來捕獲 signal 或 sigaction,通過設(shè)置的回調(diào)函數(shù)來收集信號(hào)的上下文信息。

第二種問題可以通過 NSSetUncaughtExceptionHandler 設(shè)置異常處理回調(diào)函數(shù)來收集異常的調(diào)用堆棧。

收集崩潰的上下文信息

使用 NSUncaughtExceptionHandler 捕獲 NSException

通過 void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler *) 函數(shù)設(shè)置異常發(fā)生時(shí)對(duì)應(yīng)的事件處理函數(shù),NSUncaughtExceptionHandler 是一個(gè)函數(shù)指針 typedef void NSUncaughtExceptionHandler(NSException *exception),該函數(shù)指針的入?yún)⑹?NSException,包含該異常的調(diào)用堆棧:

void InstallUncaughtExceptionHandler(void) {
    // Backup original handler
    g_previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();

    NSSetUncaughtExceptionHandler(&HandleException);
}

void MyUncaughtExceptionHandler(NSException *exception) {
    // 異常的堆棧信息
    NSArray *stackArray = [exception callStackSymbols];
    // 出現(xiàn)異常的原因
    NSString *reason = [exception reason];
    // 異常名稱
    NSString *name = [exception name];
    NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
    NSLog(@"%@", exceptionInfo);
    [UncaughtExceptionHandler saveCreash:exceptionInfo];
    
    if (g_previousUncaughtExceptionHandler != NULL) {
        g_previousUncaughtExceptionHandler(exception);
    }
}

上面是捕獲異常的簡(jiǎn)單示例。

捕獲 Signal 信號(hào)

Signal 信號(hào)是 Unix 系統(tǒng)中一種用于異步通知的機(jī)制。信號(hào)傳遞給進(jìn)程后,在沒有設(shè)置處理函數(shù)的情況下,程序可以指定三種行為:

  1. 忽略信號(hào),但 SIGKILL 和 SIGSTOP 信號(hào)不可忽略;
  2. 使用默認(rèn)的處理函數(shù) SIG_DFL,大多數(shù)信號(hào)的默認(rèn)動(dòng)作是終止進(jìn)程;
  3. 捕獲信號(hào),執(zhí)行用戶定義的函數(shù)。

這里有兩個(gè)特殊的常量:

  • SIG_IGN:向內(nèi)核表示忽略此信號(hào)。對(duì)于不能忽略的兩個(gè)信號(hào)SIGKILL和SIGSTOP,調(diào)用時(shí)會(huì)報(bào)錯(cuò);
  • SIG_DFL:執(zhí)行該信號(hào)的系統(tǒng)默認(rèn)動(dòng)作.

常用函數(shù):

  • int kill(pid_t pid, int signo) 發(fā)送信號(hào)到指定的進(jìn)程
  • int raise(int signo) 發(fā)送信號(hào)給自己

Unix 系統(tǒng)中常見信號(hào)有如下幾種:

SIGABRT--程序中止命令中止信號(hào) 
SIGALRM--程序超時(shí)信號(hào) 
SIGFPE--程序浮點(diǎn)異常信號(hào)
SIGILL--程序非法指令信號(hào)
SIGHUP--程序終端中止信號(hào)
SIGINT--程序鍵盤中斷信號(hào) 
SIGKILL--程序結(jié)束接收中止信號(hào) 
SIGTERM--程序kill中止信號(hào) 
SIGSTOP--程序鍵盤中止信號(hào)  
SIGSEGV--程序無效內(nèi)存中止信號(hào) 
SIGBUS--程序內(nèi)存字節(jié)未對(duì)齊中止信號(hào) 
SIGPIPE--程序Socket發(fā)送失敗中止信號(hào)

會(huì)導(dǎo)致程序被殺掉的有下面幾種,我們只需收集這幾種信號(hào)的上下文信息,就能找到崩潰發(fā)生原因。

SIGABRT,
SIGBUS,
SIGFPE,
SIGILL,
SIGSEGV,
SIGTRAP,
SIGTERM,
SIGKILL,

信號(hào)處理流程分三步:

  1. 注冊(cè)信號(hào)處理回調(diào)函數(shù);
  2. 在回調(diào)函數(shù)中收集調(diào)用堆棧信息;
  3. 恢復(fù)信號(hào)默認(rèn)處理函數(shù);

1.注冊(cè)信號(hào)處理回調(diào)函數(shù)

static int Beacon_errorSignals[] = {
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGSEGV,
    SIGTRAP,
    SIGTERM,
    SIGKILL,
};
for (int i = 0; i < Beacon_errorSignalsNum; i++) {
    signal(Beacon_errorSignals[i], &SignalExceptionHandler);
}

2.回調(diào)函數(shù)中收集調(diào)用堆棧信息

void SignalExceptionHandler(int sig) {
    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];
    free(strs);
}

3.恢復(fù)信號(hào)默認(rèn)處理函數(shù)

但這里會(huì)將信號(hào)不斷的發(fā)向該處理函數(shù),導(dǎo)致應(yīng)用無法正常崩潰,因?yàn)橐话愕南⑻幚頃?huì)向進(jìn)程終結(jié),但是這里沒有,所以還會(huì)有同樣地信號(hào)不斷的發(fā)過來并被處理.所以處理函數(shù)后要終結(jié)該處理函數(shù)的處理,并將其由系統(tǒng)默認(rèn)處理,即:

signal(sig, SIG_DFL);

測(cè)試

完成異常和信號(hào)處理函數(shù)的設(shè)置后,我們需要測(cè)試設(shè)置是否生效,能否正常捕獲到崩潰的堆棧信息。測(cè)試需要注意:信號(hào)時(shí)不能在 debug 環(huán)境下進(jìn)行,系統(tǒng)的 debug 會(huì)優(yōu)先攔截信號(hào)。正確的測(cè)試姿勢(shì),安裝應(yīng)用后關(guān)閉 debug,直接在模擬器中點(diǎn)擊應(yīng)用制造信號(hào)。Exception 測(cè)試可以在 debug 環(huán)境下進(jìn)行。

- (IBAction)buttonClick:(UIButton *)sender {
    //1.信號(hào)量
    Test *pTest = {1,2};
    free(pTest); //導(dǎo)致SIGABRT的錯(cuò)誤,因?yàn)閮?nèi)存中根本就沒有這個(gè)空間,哪來的free,就在棧中的對(duì)象而已
    pTest->a = 5;
}

- (IBAction)buttonOCException:(UIButton *)sender {
    //2.ios崩潰
    NSArray *array= @[@"tom",@"xxx",@"ooo"];
    [array objectAtIndex:5];
}

收集后的清理

傳遞 UncaughtExceptionHandler

如果多方通過 NSSetUncaughtExceptionHandler 注冊(cè)異常處理程序,后注冊(cè)的異常處理程序會(huì)覆蓋前一個(gè)注冊(cè)的 handler,導(dǎo)致之前注冊(cè)的日志收集服務(wù)收不到相應(yīng)的 NSException,丟失崩潰堆棧信息。(iOS 系統(tǒng)自帶的 Crash Reporter 不受影響)。

崩潰后友好退出

而對(duì)于有些時(shí)候,在iOS中,在應(yīng)用崩潰后,保持運(yùn)行狀態(tài)而不退出:

CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);

while (!dismissed) {
    for (NSString *mode in (__bridge NSArray *)allModes) {
        CFRunLoopRunInMode((__bridge CFStringRef)mode, 0.001, false);
    }
}

CFRelease(allModes);

應(yīng)用以上代碼,可以做到崩潰時(shí)彈框提示應(yīng)用,以讓用戶還是可以正常操作,讓響應(yīng)更加友好.

存在的問題

使用上述方式收集到的堆棧信息只包含錯(cuò)誤線程,其他線程的調(diào)用堆棧無法獲取。而在一些 Signal 的出錯(cuò)信息僅靠崩潰線程的堆棧無法找到原因,需同時(shí)根據(jù)其他線程調(diào)用堆棧來尋找崩潰原因。

目前成熟的開源崩潰日志收集服務(wù)有很多,如 KSCrash,PLCrashReporter,CrashKit 等,使用一番后覺得 PLCrashReporter 更符合項(xiàng)目要求。PL 收集崩潰日志信息和蘋果官方日志兼容,擴(kuò)展性較好,與已有服務(wù)銜接較為簡(jiǎn)單。

集成 PLCrashReporter

官網(wǎng)下載最新的 release 包,將iOS Framework/CrashReporter.framework 拖進(jìn)工程。在 application:didFinishLaunchingWithOptions 方法中調(diào)用 initCrashMgr 完成 PLCrashReporter 的初始化。

- (void)initCrashMgr {
    PLCrashReporter *crashReporter = [PLCrashReporter sharedReporter];
    NSError *error;
    // Check if we previously crashed
    if ([crashReporter hasPendingCrashReport]) {
        [self handleCrashReport];
    }
    // Enable the Crash Reporter
    if (![crashReporter enableCrashReporterAndReturnError: &error]) {
        ABLog(@"Warning: Could not enable crash reporter: %@", error);
    }
}

- (void)handleCrashReport {
    PLCrashReporter *crashReporter = [PLCrashReporter sharedReporter];
    NSData *crashData;
    NSError *error;
    
    // Try loading the crash report
    crashData = [crashReporter loadPendingCrashReportDataAndReturnError:&error];
    if (crashData == nil) {
        ABLog(@"Could not load crash report: %@", error);
        [crashReporter purgePendingCrashReport];
        return;
    }
    
    // We could send the report from here, but we'll just print out some debugging info instead
    PLCrashReport *report = [[PLCrashReport alloc] initWithData:crashData error:&error];
    if (report == nil) {
        ABLog(@"Could not parse crash report");
        [crashReporter purgePendingCrashReport];
        return;
    }
    
    //TODO:send the report
    ABLog(@"Crashed on %@", report.systemInfo.timestamp);
    ABLog(@"Crashed with signal %@ (code %@, address=0x%" PRIx64 ")", report.signalInfo.name, report.signalInfo.code, report.signalInfo.address);
    NSString *humanReadText = [PLCrashReportTextFormatter stringValueForCrashReport:report withTextFormat:PLCrashReportTextFormatiOS];
    
    // 處理收集到的 crash 信息
    [self sendCrashReport:humanReadText];
    
    [crashReporter purgePendingCrashReport];
    return;
}

PLCrashReporter 收集的 crash 非常全媲美蘋果的收集的日志,簡(jiǎn)單看了下源碼原理和上述思路一致,但一直沒找到它如何解決其他線程的堆棧收集問題,有時(shí)間繼續(xù)研讀下。

參考文章:

iOS崩潰信息收集
iOS異常捕獲
漫談iOS Crash收集框架

最后編輯于
?著作權(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)容

  • 前言 崩潰是讓發(fā)人員比較頭痛的事情,app崩潰了,說明代碼寫的有問題,這時(shí)如何快速定位到崩潰的地方很重要。調(diào)試階段...
    進(jìn)無盡閱讀 2,177評(píng)論 0 9
  • KSCrash 是一個(gè)異常收集的開源框架。 它可以捕獲到Mach級(jí)內(nèi)核異常、信號(hào)異常、C++異常、Objectiv...
    sincere_bs閱讀 10,436評(píng)論 11 41
  • 轉(zhuǎn)載(漫談 iOS Crash 收集框架) 前言 很早以前就和念茜認(rèn)識(shí),念茜不但技術(shù)功底扎實(shí),而且長(zhǎng)得很漂亮,說她...
    狂風(fēng)無跡閱讀 3,582評(píng)論 1 11
  • 比較好的轉(zhuǎn)載:http://www.cocoachina.com/ios/20151218/14748.html轉(zhuǎn)...
    liudhkk閱讀 983評(píng)論 0 2
  • 前言 iOS崩潰是讓iOS開發(fā)人員比較頭痛的事情,app崩潰了,說明代碼寫的有問題,這時(shí)如何快速定位到崩潰的地方很...
    齊滇大圣閱讀 65,888評(píng)論 29 443

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