iOS Crash處理方法(二):自己編寫代碼定位Crash

這篇接著iOS Crash處理方法(一):MethodSwizzle 繼續描述Crash的問題。
這篇跟上篇不同,上篇是講述如何避免Crash的問題,而這篇則是把Crash暴露出來,定位Crash位置。


本篇文章完整代碼的github地址:https://github.com/WalkingToTheDistant/ErrorManager


上篇講述了替換系統庫API的IMP ,變成自定義API的IMP,這種方式在測試的時候好像是完美地解決了多數內存越界的問題,怎么測試就怎么爽,可是真正上線了,突然Crash數直飚,而且在iOS8 的Crash率奇高,而且還無法定位問題代碼!!!
報錯的信息大致是這樣的:


屏幕快照 2017-08-24 15.25.02.png

好吧,任我怎么測試也沒有出現類似的Crash,看來不能再用這種投機取巧、無底線浪的方法了,踏踏實實改BUG吧。

改BUG就要收集Crash報告,然后才能找到在哪報錯了呀,而目前收集Crash報告的方法一般有這幾種方法:

  • Xcode 的Crash收集:這個是iOS 系統自帶的Crash報告上傳,優點在于可以直接在Xcode里面定位代碼,缺點就是需要用戶同意,而且流程時間一般要好幾天:從用戶的手機上傳到蘋果服務器,服務器匯總再轉發給我們的Xcode。
  • 第三方Crash收集SDK:目前比較常用的有友盟和騰訊bugly,他們會匯集Crash的詳細信息,比如iOS版本號以及Crash log,優點是能夠很快獲取到Crash log,不過有一小部分的log還是無法定位出BUG代碼。
  • 第三種方法是導出Iphone本地存儲的Crash log,一般APP發生Crash的時候,iOS會存儲相關Crash的信息到本地,這時候需要使用Xcode連接該iphone,然后使用XCode->windows->Devices->view Device Logs,或者用Itunes同步手機獲取信息,就可以看到相關APP的Crash的信息。(這個需要符號化Crash信息)
  • 額,Xcode連接手機運行項目,等待Crash的方法就不扯了…

通過CrashAddress 定位問題的方式也有幾種,不過目前比較常用的是atos命令從Symbol(Symbol文件是Xcode打包之后自動生成的符號表,記錄著每行代碼的位置信息。),在最后分析環節再說明如何使用atos定位問題。

現在我們來說說第三方一般都是怎么捕獲異常信息,然后也就解釋了友盟上很多Crash log定位的代碼最終都指向了UmengSignalhandler。

捕獲異常

(1)Mach異常 和 Unix信號

關于Mach和Unix的具體信息,念茜大神這篇講解的很詳細:漫談iOS Crash收集框架。簡單來說,Mach異常是指最底層的內核級異常,而為了更直觀友好的展示異常,就把Mach異常轉換成了Unix信號,所以也就存在這種情況:Mach異常處理會先于Unix信號處理發生,如果Mach異常的handler讓程序exit了,那么Unix信號就永遠不會到達這個進程了。例如,訪問野指針導致的EXC_BAD_ACCESS問題,Mach會轉換成 SIGSEGV 的Unix信號。
這篇文章暫時只處理Unix信號,捕獲Unix信號的方法:

/* 第一個參數是需要捕獲的信號值,第二個參數是處理函數的函數指針,也就是你寫的捕獲方法 */
void (*signal(int, void (*)(int)))(int);
#import <sys/signal.h>
/** 信號處理函數 */
void handleSignalHandler(int signalValue){

}
signal(SIGSEGV, handleSignalHandler); // 監聽 SIGSEGV 信號

/* 常用的信號值說明
* SIGABRT :由于abort()函數調用發生的程序中止信號
* SIGILL:由于非法指令產生的程序中止信號
* SIGSEGV:由于無效內存的引用導致的程序中止信號
* SIGFPE:由于浮點數異常導致的程序中止信號
* SIGBUS:由于內存地址未對齊導致的程序中止信號
* SIGPIPE:程序通過端口發送消息失敗導致的程序中止信號(這個信號比較特殊,后面會提到)
*/
(2)NSException 拋出

單單依靠捕獲Unix信號是不夠的,因為還有一種情況會導致Crash,那就是拋出的Objective-C異常報告(NSException)。這種Crash就不會觸發上面說的Unix信號,所以還要單獨處理這種情況。
捕獲 NSException 的方法:

/* 參數同樣為異常處理方法的函數指針 */
void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable);
#import <Foundation/NSException.h>
void uncaughtExceptionHandler (NSException *exception){

}
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler); // 使用方法

獲取異常信息

(1)Unix 信號

當我們設置監聽的信號發出來的時候(這時候一般是Crash了),我們的回調函數handleSignalHandler 會被觸發,但是handleSignalHandler方法只有一個int參數,那就是信號值,我們想獲取更多Crash信息的話,需要從堆棧中取出信息。

#include <execinfo.h>
void handleSignalHandler(int signalValue){
    void* callstack[128];
    int frames = backtrace(callstack, 128);  // Linux下,該函數用于獲取當前線程的調用堆棧,獲取的信息將會被存放在buffer中,它是一個指針列表。參數size 用來指定buffer中可以保存多少個void* 元素。函數返回值是實際獲取的指針個數,最大不超過size大小。

    char** strs = backtrace_symbols(callstack, frames); // backtrace_symbols將從backtrace函數獲取的信息轉化為一個字符串數組;參數buffer應該是從backtrace函數獲取的指針數組,size是該數組中的元素個數(backtrace的返回值)。

    // 轉換好了,這樣strs是一個指向字符串數組的指針,它的大小同buffer相同, 每個字符串包含了一個相對于buffer中對應元素的可打印信息.它包括函數  名,函數的偏移地址,和實際的返回地址
    for (int i = 0; i <frames; i+=1) {
        NSString *strTemp = [NSString stringWithUTF8String:strs[i]];
        NSLog(@"%@", strTemp); // 可以直接輸出信息
    }
    free(strs); // 用完了要記得釋放
    kill(getpid(), signalValue); // 最后要殺掉進程這句話不能少,不然你會看到你的APP卡死在那里
}
(2)NSExceoption

這個就獲取就比較容易了,NSExceoption對象本身就存儲了異常信息(callStackSymbols),我們要做的就是從callStackSymbols中輸出信息就可以了。

/** 這是 NSException 的聲明 */
@interface NSException : NSObject <NSCopying, NSCoding> 
@property (readonly, copy) NSExceptionName name;
@property (nullable, readonly, copy) NSString *reason;
@property (nullable, readonly, copy) NSDictionary *userInfo;

@property (readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses NS_AVAILABLE(10_5, 2_0);
@property (readonly, copy) NSArray<NSString *> *callStackSymbols NS_AVAILABLE(10_6, 4_0);
#import <Foundation/NSException.h>

void uncaughtExceptionHandler (NSException *exception){
    NSLog(@"%@", exception.callStackSymbols.description);

    NSSetUncaughtExceptionHandler(NULL); // 最后取消監聽,用完就回收嘛
}

異常信息分析

墨跡了那么久,終于開始進入重點啦。



我先寫個常見的錯誤代碼,然后看看輸出的異常信息是什么樣的:

- (void) clickBtn:(UIButton*)btn
{
    NSArray *ary = [NSArray arrayWithObjects:@"dsfs", @"dsfsd", nil];
    NSLog(@"%@", ary[4]);  // 會觸發 uncaughtExceptionHandler 方法
}

輸出結果:

callStackSymbols:
(
    0   CoreFoundation    0x0000000103c12b0b __exceptionPreprocess + 171
    1   libobjc.A.dylib   0x00000001035cf141 objc_exception_throw + 48
    2   CoreFoundation    0x0000000103b5038b -[__NSArrayI objectAtIndex:] + 155
    3   TempPro           0x0000000102ff1f94 -[ViewController clickBtn:] + 242
    4   UIKit             0x0000000104037d82 -[UIApplication sendAction:to:from:forEvent:] + 83
    5   UIKit             0x00000001041bc5ac -[UIControl sendAction:to:forEvent:] + 67
    6   UIKit             0x00000001041bc8c7 -[UIControl _sendActionsForEvents:withEvent:] + 450
    7   UIKit             0x00000001041bb802 -[UIControl touchesEnded:withEvent:] + 618
    8   UIKit             0x00000001040a57ea -[UIWindow _sendTouchesForEvent:] + 2707
    9   UIKit             0x00000001040a6f00 -[UIWindow sendEvent:] + 4114
    10  UIKit             0x0000000104053a84 -[UIApplication sendEvent:] + 352
    11  UIKit             0x00000001048375d4 __dispatchPreprocessedEventFromEventQueue + 2926
    12  UIKit             0x000000010482f532 __handleEventQueue + 1122
    13  CoreFoundation    0x0000000103bb8c01 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    14  CoreFoundation    0x0000000103b9e0cf __CFRunLoopDoSources0 + 527
    15  CoreFoundation    0x0000000103b9d5ff __CFRunLoopRun + 911
    16  CoreFoundation    0x0000000103b9d016 CFRunLoopRunSpecific + 406
    17  GraphicsServices  0x000000010793fa24 GSEventRunModal + 62
    18  UIKit             0x0000000104036134 UIApplicationMain + 159
    19  TempPro           0x0000000102ff28ff main + 80
    20  libdyld.dylib     0x00000001069d465d start + 1
) 

前5行(0 ~ 4)一般都是最貼近你代碼里面的錯誤信息,這個異常比較容易,一看就大概知道什么原因(2 :[__NSArrayI objectAtIndex:]+ 155),以及什么位置了(3:[ViewController clickBtn:] + 242)。那如果想要更精確地確定是哪一行代碼出現了問題呢,那就需要使用atos命令進行解析(后面的+ 242 可不是代碼行數,不要那么耿直…)。
首先我們要從這里提取重要信息,這里哪一行信息才是我們需要的呢?首先前面標有CoreFoundation、 libobjc.A.dylib這些信息我們就不用看了,都是系統庫問題,你也找不到,我們要找的是 標有我們工程名的那一行,上面的例子中就是 TempPro 這一行,一般在0~5行,越往后定位越遠…

3   TempPro  0x0000000102ff1f94 -[ViewController clickBtn:] + 242

我們要的是這個地址:0x0000000102ff1f94,下面開始說明如何用這個地址。
(1)拿到dSYM文件
首先我們要拿到dSYM文件,dSYM文件是每次打包安裝包的時候,會自動生成的,里面類似一張列表,對應存儲著我們代碼的位置。獲取方法:Xcode -> Window -> Organrizer -> Archives -> 你的項目名(TempPro) -> 右鍵"Show in Finder" -> 右鍵TempPro. xcarchive 文件 -> 顯示包內容 -> dSYMs -> TempPro.app.dSYM -> 右鍵顯示包內容 -> Contents -> Resources -> DWARF -> TempPro(就是這個啦)(注:TempPro都是你的工程Targets名)把TempPro拷貝出來放在一個文件夾下。

(2)獲取dSYM內存地址
因為iOS采用了ASLR(Address space layout randomization),ASDL機制會在app加載時根據load address動態加一個偏移地址slide address。所以在捕獲錯誤地址stack address后,需要減去偏移地址才能得到正確的符號地址symbol address,上面我們從異常信息提取到的地址0x0000000102ff1f94 是stack address。額,簡單說,dSYM里面存儲的是一張連續的內存地址表(0~10),但是iOS 運行時從中拿了一部分(3~5),然后加載到一個起始內存地址為20的內存塊中,那么我們從異常信息中提取到的內存地址是 (23 ~ 25),這樣是無法映射到dSYM表(0~10)中,就需要減去一個偏移量(就是起始內存地址20),之后才可以在dSYM表中找到該代碼的位置了。
下面是獲取偏移量(偏移地址slide address)的代碼:

/** 獲取加載偏移地址 */
long long getSlide()
{
    long long slide = 0;
    for (uint32_t i = 0; i < _dyld_image_count(); i++) {
        if (_dyld_get_image_header(i)->filetype == MH_EXECUTE) {
            slide = _dyld_get_image_vmaddr_slide(i);
            break;
        }
    }
    return slide;
}

我們假設獲取到的地址slide address = 0x00f2d33
那么最終符號地址symbol address = 0x0000000102ff1f94 - 0x00f2d33 = 0x102EFF261

(3)atos命令分析
先看下atos命令

atos -arch arm64 -o /temp/TempPro 0x102EFF261

其中arm64是指CPU的型號,這個就需要根據APP是在哪個手機上運行決定的,這里有個型號對應表

armv6:iPhone、iPhone2、iPhone3G
armv7:iPhone4、iPhone4S
armv7s:iPhone5、iPhone5C
arm64:iPhone5S

而 /temp/TempPro 這個是剛才我們第一步提取到的dSYM文件的存放位置(我放在一個temp文件夾下)
而 0x102EFF261 這個就是我們要輸入的symbol address啦(第二步獲取到)
那么根據上面的步驟,我們最終要執行的atos命令如下:

atos -arch arm64 -o /temp/TempPro 0x102EFF261

執行之后,輸出結果(上面都是示例地址,下面這個是我實際測試用的)


屏幕快照 2017-09-06 15.17.52.png
-[ViewController clickBtn:] (in TempPro) (ViewController.m:267)

這句話就很清晰啦,ViewController.m 是位置文件,m:267就是代碼行數。
納尼!你不知道怎么顯示行數?!………………
Xcode -> Preferencs -> Text Editing -> Line numbers 勾選

(4)注意事項
有時候在獲取異常信息 callStackSymbols 的時候,是無法獲取到具體問題代碼位置的信息,是因為某些編譯器的優化選項對獲取正確的調用堆棧有干擾,另外內聯函數沒有堆棧框架,或者刪除框架指針,這些原因都會導致無法正確解析堆棧內容。(如果用友盟方法,經常定位到UmengSignalHandler就是這種情況(如果是上面的代碼,就是定位到handleSignalHandler方法))。
另外關于 SIGPIPE 信號,是關于iOS Socket長鏈接,APP處于前臺然后鎖屏,再重新解鎖打開APP,會Crash的問題,可以查看這篇文章
如何在 iOS 上避免 SIGPIPE 信號導致的 crash (Avoiding SIGPIPE signal crash in iOS)
解決辦法,在APP剛啟動時執行這句代碼

signal(SIGPIPE, SIG_IGN);
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,963評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,348評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,083評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,706評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,442評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,802評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,983評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,542評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,287評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,486評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,710評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,116評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,412評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,224評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,462評論 2 378

推薦閱讀更多精彩內容