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

大家平時在開發(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)自己的崩潰日志收集服務筐高,成熟的開源項目很多,如 KSCrashplcrashreporter柑土,CrashKit 等蜀肘。追求方便省心,對于保密性要求不高的程序來說稽屏,也可以選擇各種一條龍Crash統(tǒng)計產(chǎn)品扮宠,如 CrashlyticsHockeyapp 狐榔,友盟坛增,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);
}

關于Signal參考

我們從上面的函數(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鸥拧,和諧學習党远,不急不躁!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末富弦,一起剝皮案震驚了整個濱河市沟娱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌腕柜,老刑警劉巖济似,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異盏缤,居然都是意外死亡砰蠢,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門唉铜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來台舱,“玉大人,你說我怎么就攤上這事潭流【和铮” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵灰嫉,是天一觀的道長拆宛。 經(jīng)常有香客問我,道長熬甫,這世上最難降的妖魔是什么胰挑? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮椿肩,結果婚禮上瞻颂,老公的妹妹穿的比我還像新娘。我一直安慰自己郑象,他們只是感情好贡这,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著厂榛,像睡著了一般盖矫。 火紅的嫁衣襯著肌膚如雪丽惭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天辈双,我揣著相機與錄音责掏,去河邊找鬼。 笑死湃望,一個胖子當著我的面吹牛换衬,可吹牛的內容都是我干的。 我是一名探鬼主播证芭,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼瞳浦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了废士?” 一聲冷哼從身側響起叫潦,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎矗蕊,沒想到半個月后拔妥,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了洼裤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片值骇。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡移国,死狀恐怖吱瘩,靈堂內的尸體忽然破棺而出蜜徽,到底是詐尸還是另有隱情拘鞋,我是刑警寧澤掐禁,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布峡扩,位于F島的核電站教届,受9級特大地震影響买置,放射性物質發(fā)生泄漏忿项。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望榨为。 院中可真熱鬧,春花似錦板壮、人聲如沸绰精。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至裕寨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間庆猫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工拳锚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留匾荆,地道東北人牙丽。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓析校,卻偏偏與公主長得像遂唧,于是被迫代替她去往敵國和親纹烹。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內容