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

本文轉載於:做者:Cooci_和諧學習_不急不躁
連接:www.jianshu.com/p/56f96167a…git

你們平時在開發過程當中,常常會遇到Crash,那也是在正常不過的事,可是做爲一個優秀的iOS開發人員,必將這些用戶不良體驗降到最低。github

  • 線下Crash,咱們直接能夠調試,結合stack信息,不難定位!
  • 線上Crash固然也有一些信息,畢竟蘋果爸爸的產品仍是作得很是不錯的!

經過iPhone的Crash log也能夠分析一些,可是這個是須要用戶配合的,由於須要用戶在手機 中 設置-> 診斷與用量->勾選 自動發送 ,而後在xcode中 Window->Organizer->Crashes 對應的app,就是當前app最新一版本的crash log ,而且是解析過的,能夠根據crash 棧 等相關信息 ,尤爲是程序代碼級別的 有超連接,一鍵能夠直接跳轉到程序崩潰的相關代碼,這樣更容易定位bug出處.面試

爲了可以第一時間發現程序問題,應用程序須要實現本身的崩潰日誌收集服務,成熟的開源項目不少,如 KSCrashplcrashreporterCrashKit 等。追求方便省心,對於保密性要求不高的程序來講,也能夠選擇各類一條龍Crash統計產品,如 CrashlyticsHockeyapp友盟Bugly 等等算法

可是,全部的可是,這不夠!由於咱們再也不是一個簡單會用的iOS開發人員,必將走向底層,瞭解原理,掌握裝逼內容和技巧是咱們的必修課swift

首先咱們來了解一下Crash的底層原理

iOS系統自帶的 Apple’s Crash Reporter記錄在設備中的Crash日誌,Exception Type項一般會包含兩個元素:Mach異常Unix信號數組

Exception Type:         EXC_BAD_ACCESS (SIGSEGV)    
Exception Subtype:      KERN_INVALID_ADDRESS at 0x041a6f3
複製代碼

Mach異常是什麼?它又是如何與Unix信號創建聯繫的?xcode

Mach是一個XNU的微內核核心,Mach異常是指最底層的內核級異常,被定義在下 。每一個thread,task,host都有一個異常端口數組,Mach的部分API暴露給了用戶態,用戶態的開發者能夠直接經過Mach API設置thread,task,host的異常端口,來捕獲Mach異常,抓取Crash事件。bash

全部Mach異常都在host層被ux_exception轉換爲相應的Unix信號,並經過threadsignal將信號投遞到出錯的線程。iOS中的 POSIX API就是經過Mach之上的 BSD層實現的。服務器

所以, EXC_BAD_ACCESS (SIGSEGV)表示的意思是: Mach層的 EXC_BAD_ACCESS異常,在 host層被轉換成 SIGSEGV信號投遞到出錯的線程。

iOS的異常Crash

  • KVO問題微信

  • NSNotification線程問題

  • 數組越界

  • 野指針

  • 後臺任務超時

  • 內存爆出

  • 主線程卡頓超閥值

  • 死鎖

    ....

    下面我就拿出最多見的兩種Crash分析一下

  • Exception

  • Signal

Crash分析處理

上面咱們也知道:既然最終以信號的方式投遞到出錯的線程,那麼就能夠經過註冊相應函數來捕獲信號.到達Hook的效果

+ (void)installUncaughtSignalExceptionHandler{
    NSSetUncaughtExceptionHandler(&LGExceptionHandlers);
    signal(SIGABRT, LGSignalHandler);
}
複製代碼

關於Signal參考

咱們從上面的函數能夠Hook到信息,下面咱們開始進行包裝處理.這裏仍是面向統一封裝,由於等會咱們還須要考慮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進行處理,在這裏要作兩件事

  • 存儲,上傳:方便開發人員檢查修復
  • 處理Crash奔潰,咱們也不能眼睜睜看着BUG閃退在用戶的手機上面,但願「起死回生,迴光返照」
- (void)lg_handleException:(NSException *)exception{
    // crash 處理
    // 存
    NSDictionary *userInfo = [exception userInfo];
    [self saveCrash:exception file:[userInfo objectForKey:LGUncaughtExceptionHandlerFileKey]];
}

複製代碼

下面是一些封裝的一些輔助函數

  • 保存奔潰信息或者上傳:針對封裝數據本地存儲,和相應上傳服務器!
- (void)saveCrash:(NSException *)exception file:(NSString *)file{
    
    NSArray *stackArray = [[exception userInfo] objectForKey:LGUncaughtExceptionHandlerCallStackSymbolsKey];// 異常的堆棧信息
    NSString *reason = [exception reason];// 出現異常的緣由
    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);
}
複製代碼
  • 獲取函數堆棧信息,這裏能夠獲取響應調用堆棧的符號信息,經過數組回傳
+ (NSArray *)lg_backtrace{
    
    void* callstack[128];
    int frames = backtrace(callstack, 128);//用於獲取當前線程的函數調用堆棧,返回實際獲取的指針個數
    char **strs = backtrace_symbols(callstack, frames);//從backtrace函數獲取的信息轉化爲一個字符串數組
    int i;
    NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
    for (i = LGUncaughtExceptionHandlerSkipAddressCount;
         i < LGUncaughtExceptionHandlerSkipAddressCount+LGUncaughtExceptionHandlerReportAddressCount;
         i++)
    {
        [backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
    }
    free(strs);
    return backtrace;
}

複製代碼
  • 獲取應用信息,這個函數提供給Siganl數據封裝
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

下面咱們來一個騷操做:在監聽的信息的時候來了一個Runloop,咱們監聽全部的mode,開啓循環(一個相對於咱們應用程序自啓的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);
複製代碼

在這個平行空間咱們開啓一個彈框,這個彈框,跟着咱們的應用程序保活,而且具有相應的響應能力,到目前爲止:此時此刻還有誰!這不就是迴光返照?只要咱們的條件成立,那麼在相應的這個平行空間繼續作一些咱們的工做,程序不死:what is dead may never die,but rises again harder and stronger

signal 函數攔截不到的解決方式

在debug模式下,若是你觸發了崩潰,那麼應用會直接崩潰到主函數,斷點都沒用,此時沒有任何log信息顯示出來,若是你想看log信息的話,你須要在lldb中,拿SIGABRT來講吧,敲入pro hand -p true -s false SIGABRT命令,否則你啥也看不到。

而後斷開斷點,程序進入監聽,下面剩下的操做就是包裝異常,操做相似Exception

最後咱們須要注意的針對咱們的監聽回收相應內存:

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處理已經入門,若是你還想繼續探索也是有不少地方好比:

sdsdsdssd

  • 咱們可否hook系統奔潰,異常的方法NSSetUncaughtExceptionHandler,已達到拒絕傳遞 UncaughtExceptionHandler的效果

  • 咱們在處理異常的時候,利用Runloop迴光返照,有沒有更加合適的方法

  • Runloop迴光返照咱們怎麼繼續保證應用程序穩定執行

想學習數據結構與算法、底層進階、swift、逆向、底層面試題整合文檔等的同窗能夠加我微信JuinDay,免費提供學習資料!

相關文章
相關標籤/搜索