淺談 iOS Crash(一)

2021-02-24 iOS大全

(點擊上方公眾號,可快速關注)

來源:南華Coder   自薦投稿

www.jianshu.com/p/3261493e6d9e

如有好文章投稿,請點擊 → 這裡了解詳情

一、捕獲iOS Crash

1、設置異常斷點並運行

設置異常斷點.png

說明:設置Xcode異常斷點後運行程序,發生Crash時,斷點會定位到出錯的代碼行,但僅適用於開發階段。線上APP的Crash還需要通過收集Crash機制來捕獲Crash並記錄在日誌中。

2、Mach異常 和 Unix信號

iOS Crash發生時,先產生Mach異常(最底層的內核級異常),然後Mach異常在host層被ux_exception轉換為相應的Unix信號,並通過threadsignal將信號投遞到出錯的線程。

在捕獲Crash事件時,優選Mach異常。因為Mach異常處理會先於Unix信號處理發生,如果Mach異常的handler讓程序exit了,那麼Unix信號就永遠不會到達這個進程了。而轉換Unix信號是為了兼容更為流行的POSIX標準(SUS規範),這樣就不必了解Mach內核也可以通過Unix信號的方式來兼容開發。

在方案實現時,通過捕獲Mach異常+Unix信號組合方式來捕獲Crash事件。在選擇具體方案時,可以選擇PLCrashReporter這樣優秀的開源項目,也可以選擇友盟、Bugly 這類完善的Crash上報和統計的產品(試項目需求而定)。

3、捕獲Crash

並不是所有的Crash都可以捕獲到NSException,如果捕獲不到,可以使用signal機制來捕獲Crash發生時的錯誤內容。

1) 可以捕獲的NSException,通過註冊NSUncaughtExceptionHandler捕獲異常信息

//註冊異常處理函數

NSSetUncaughtExceptionHandler(&uncaught_exception_handler);

//異常處理函數

static void uncaught_exception_handler (NSException *exception) {

  //可以取到 NSException 信息

  //...

  abort();

}

說明: 使用Objective-C的異常處理是不能得到signal的。

2) 無法捕獲的NSException,利用Unix標準的signal機制,註冊SIGABRT, SIGBUS, SIGSEGV等信號發生時的處理函數。

//註冊處理SIGSEGV信號

signal(SIGSEGV,handleSignal);

// 註冊處理其他信號 ....

 

//信號處理函數

static void handleSignal( int sig ) {

}

二、Crash日誌組成

上部分介紹了Crash的捕獲,這部分來看看Crash日誌的組成。

1、日誌內容Demo

日誌主要分為六個部分:進程信息、基本信息、異常信息、線程回溯、線程狀態和二進位映像。下面是從某APP具體的Crash日誌抽出的主要信息,展示如下:

//1、進程信息

Hardware Model: iPhone9,2

Process: AppName [3580]

Path: /var/containers/Bundle/Application/C7B90C8A-E269-4413-A011-552971D1ED39/AppName.app

Identifier: xxxx.xxx.xxxx.xxx

Version: xx.xx

Code Type: ARM-64 (Native)

Parent Process:  [1]

 

//2、基本信息

Date/Time: 2017-05-22 03:05:06.743 +0800

OS Version: iPhone OS 10.2.1 (14D27)

 

//3、異常信息

Exception Type: NSInvalidArgumentException(SIGABRT)

Exception Codes: -[NSNull integerValue]: unrecognized selector sent to instance 0x1a9d88ef8 at 0x00000001835c7014

Crashed Thread: 0

 

//4、線程回溯 (展示發生Crash線程的回溯信息,其他略)

Thread 0 Crashed:

0  libsystem_kernel.dylib         0x00000001835c7014 __pthread_kill + 4

1  libsystem_c.dylib              0x000000018353b400 abort + 140

2  AppName                         0x0000000100a26704 0x0000000100028000 + 10479360

3  CoreFoundation                 0x00000001845f9538 ___handleUncaughtException +  644

2  CoreFoundation                 0x0000000184600268 ___methodDescriptionForSelector

3  CoreFoundation                 0x00000001845fd270 ____forwarding___ +  916

4  CoreFoundation                 0x00000001844f680c _CF_forwarding_prep_0 + 80

5  AppName                         0x0000000100205280 0x0000000100028000 + 1954432

6  AppName                         0x00000001002ae59c 0x0000000100028000 + 2647440

7  AppName                         0x0000000100482944 0x0000000100028000 + 4565312

16 CoreFoundation                 0x00000001845a6810 ___CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ +  12

     +  12

17 CoreFoundation                 0x00000001845a43fc ___CFRunLoopRun +  1660

18 CoreFoundation                 0x00000001844d22b8 CFRunLoopRunSpecific + 436

 

//5、進程狀態(展示部分)

Thread 0 crashed with ARM 64 Thread State:

     x0:  000000000000000000    x1: 000000000000000000    x2: 000000000000000000     x3: 0xffffffffffffffff

     x4:  0x0000000000000010    x5: 0x0000000000000020    x6: 000000000000000000     x7: 000000000000000000

     x8:  0x0000000008000000    x9: 0x0000000004000000   x10: 000000000000000000    x11: 0x00000001ac336c83

    x12: 0x00000001ac336c83    x13: 0x0000000000000018   x14: 0x0000000000000001    x15: 0x0000000000000881

    x16: 0x0000000000000148    x17: 000000000000000000   x18: 000000000000000000    x19: 0x0000000000000006

 

//6、二進位映像 (展示部分)

Binary Images:

0x100028000 - 0x1011dbfff +AppName arm64  /var/containers/Bundle/Application/C7B90C8A-E269-4413-A011-552971D1ED39/AppName.app/AppName

0x18368a000 - 0x183693fff  libsystem_pthread.dylib arm64  /usr/lib/system/libsystem_pthread.dylib

0x1835a8000 - 0x1835ccfff  libsystem_kernel.dylib arm64  /usr/lib/system/libsystem_kernel.dylib

0x1834b1000 - 0x1834b5fff  libdyld.dylib arm64  /usr/lib/system/libdyld.dylib

0x1834d8000 - 0x183556fff  libsystem_c.dylib arm64  /usr/lib/system/libsystem_c.dylib

0x183481000 - 0x1834b0fff  libdispatch.dylib arm64  /usr/lib/system/libdispatch.dylib

0x183028000 - 0x183401fff  libobjc.A.dylib arm64  /usr/lib/libobjc.A.dylib

2、日誌內容組成分析

整個日誌內容中,直接和Crash信息相關,最能幫助開發者定位問題部分是: 異常信息 和 線程回溯部分的內容。

1) 進程信息:發生Crash閃退進程的相關信息

Hardware Model : 標識設備類型。 如果很多崩潰日誌都是來自相同的設備類型,說明應用只在某特定類型的設備上有問題。上面的日誌裡,崩潰日誌產生的設備是iPhone 7 Plus (iPhone 7 Plus 也是2個版本 iPhone9,2 和 iPhone9,4. 硬體代號為 D11AP 和 D111AP. 型號有: A1661, A1784, A1785 和 A1786. )

Process 是應用名稱。中括號裡面的數字是閃退時應用的進程ID。

2) 基本信息:給出了一些基本信息,包括閃退發生的日期和時間,設備的iOS版本。

3) 異常信息:閃退發生時拋出的異常類型。還能看到異常編碼和拋出異常的線程。

//以上面內容中的異常信息為例:

Exception Type: NSInvalidArgumentException(SIGABRT)

Exception Codes: -[NSNull integerValue]: unrecognized selector sent to instance 0x1a9d88ef8 at 0x00000001835c7014

Crashed Thread: 0

4) 線程回溯:回溯是閃退發生時所有活動幀清單。它包含閃退發生時調用函數的清單。

5) 線程狀態:閃退時寄存器中的值。一般不需要這部分的信息,因為回溯部分的信息已經足夠讓你找出問題所在。

6) 二進位映像:閃退時已經加載的二進位文件。

三、異常信息解讀

1、Exception Type(異常類型)

異常類型可能的原因調試方法EXC_CRASHunrecognized selectorAll Exception PointEXC_BAD_ACCESS內存訪問錯誤NSZombieSIGSEGV引用了released對象 / 引用未init的對象 / 數組越界/ 試圖往沒有寫權限的內存地址寫數據NSZombieSIGABRT邏輯錯誤導致的Crash,比如嘗試多次釋放同一個沒存邏輯檢查SIGPIPETCP突然斷開,再發送數據添加signal(SIGPIPE,XX)

具體信號說明參見iOS異常捕獲(http://www.iosxxx.com/blog/2015-08-29-iosyi-chang-bu-huo.html)

2、Exception Code(異常編碼)

異常編碼描述0x8badf00date bad food ,表示應用是因為發生watchdog超時而被iOS終止的。通常是應用花費太多時間而無法啟動、終止或響應用系統事件。0xdeadfa11dead fall,用戶強制退出。0xbaaaaaad用戶按住Home鍵和音量鍵,獲取當前內存狀態,不代表崩潰。0xbad22222VoIP 應用因為過於頻繁重啟而被終止0xc00010ffcool off,因為太燙了被幹掉0xdead10ccdead lock,表明應用因為在後臺運行時佔用系統資源(如通訊錄資料庫)0xbbadbeefbad beef,發生致命錯誤

說明1:詳細的異常編碼代表的含義請參考:Hexspeak

說明2:在後臺任務列表中關閉已掛起的應用不會產生崩潰日誌。 因為應用一旦被掛起,它何時被終止都是合理的。所以不會產生崩潰日誌。

四、Crash日誌符號化

1、概述

線程回溯部分內容如下:

5  AppName                         0x0000000100205280 0x0000000100028000 + 1954432

6  AppName                         0x00000001002ae59c 0x0000000100028000 + 2647440

這兩條記錄包括四列:(以第一條記錄為例子)

幀編號—— 5(數字越小,發生時間越晚,發生順序越往後,越好鎖定問題的範圍)

二進位庫的名稱 ——此處是 AppName.

調用方法的地址 ——此處是 0x0000000100205280.

第四列分為兩個子列,一個基本地址和一個偏移量。此處是 x0000000100028000 + 1954432, 第一個數字指向文件,第二個數字指向文件中的代碼行。

說明1:線程回溯部分並不是我們習慣使用方法名和行數,而是十六進位地址。所以我們在分析Crash前需要將這些十六進位地址轉化成方法名稱和行數,改過程被稱為符號化。

說明2:符號化Crash日誌需要獲取對應的應用二進位文件以及生成二進位文件時產生的 .dSYM 文件(符號表)。必需完全匹配才行。否則,日誌將無法被完全符號化。

說明3: Xcode編譯項目後,會得到同名的 dSYM 文件(符號表),dSYM 文件(符號表)是保存 16 進位函數地址映射信息的中轉文件,我們調試的 symbols 都會包含在這個文件中,並且每次編譯項目的時候都會生成一個新的 dSYM 文件,位於 /Users//Library/Developer/Xcode/Archives 目錄下,對於每一個發布版本我們都很有必要保存對應的 Archives 文件。

說明4:符號化可以使用Xcode的兩種命令 symbolicatecrash命令 + atos命令

2、symbolicatecrash命令

1)首選找到symbolicatecrash命令的位置

find /Applications -name symbolicatecrash -type f  

  //我的本機命令的位置:/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash

2)找到線上版本對應的xcarchive文件。從中找到.dSYM和.app文件

xcarchive所在的路徑一般在: /Users//Library/Developer/Xcode/Archives 目錄下

3)獲取crash日誌文件

4)將symbolicatecrash、.dSYM、.app、crash.crash拷貝到桌面下同一個文件夾下

5)檢查 xx.app 和 xx.app.dSYM 文件以及crash 文件這三種的 UUID是否一致。

dwarfdump --uuid xx.app/xx (xx代表你的項目名)

dwarfdump --uuid xx.app.dSYM

查看crash 日誌中的Incident Identifier (crash 文件的 UUID)

6)使用命令,生成「可定位問題的crash文件」

//symbolreportXXX.crash就是符號化後的文件

./symbolicatecrash crashXXX.crash appName.app.dSYM > symbolreportXXX.crash

7) 根據符號化後的線程回溯信息,可以幫助定位出問題的代碼行。

說明:如果執行symbolicatecrash命令出現 Error: 「DEVELOPER_DIR」 is not defined at ./symbolicatecrash…這樣的錯誤,可以在執行命令前,輸入export DEVELOPER_DIR=」/Applications/XCode.app/Contents/Developer」

3、atos命令

在符號化時候,還可以使用atos命令。發現armv7處理器上的crash使用symbolicatecrash無法符號化。

1)將.dSYM、.app、crash.crash放到同一個文件夾下。

2) 知道crash文件的UUID:執行grep 「AppName arm」 *crash,得到結果

crash1.crash:0x100040000 - 0x100e23fff +AppName arm64  /var/containers/Bundle/Application/55A4D641-847F-4D24-86E1-129B28461858/AppName.app/AppName

crash2.crash:0x100060000 - 0x100e43fff +AppName arm64  /var/containers/Bundle/Application/3229ED68-8D19-406D-A3F5-EC0310C9DB7C/QAppName.app/AppName

crash3.crash:    0x5000 -   0xce8fff +AppName armv7  /var/containers/Bundle/Application/C6BE271D-2EAC-42C0-8E72-4523F88C76B2/AppName.app/AppName

其中0x100040000、0x100060000、0x5000是加載地址(loadingAddress), 而arm64、armv7 是 architecture 的值(architectureValue),這兩個值後面都要用。

3)然後執行atos命令,輸入成功,進入待輸入狀態

xcrun atos -o appName.app.dSYM/Contents/Resources/DWARF/appName -l loadingAddress -arch architectureValue

4) 此時輸入App對應的Crash地址,得到發生crash的信息。

實例1:

grep "AppName arm" *crash

    xcrun atos -o AppName.app.dSYM/Contents/Resources/DWARF/AppName -l 0x100040000 -arch arm64

實例2:

grep "AppName arm" *crash

    xcrun atos -o AppName.app.dSYM/Contents/Resources/DWARF/AppName -l 0x5000 -arch armv7

五、常見的Crash

有一些Crash比較常見,下面羅列出5種常見的Crash。

1、數組操作

@implementation NSMutableArray (Safe)

 

- (id)safeObjectAtIndex:(NSUInteger)index{

 

     if (index < self.count){

         return [self objectAtIndex:index];

     }else{

         NSLog(@"警告:數組越界!!!");

     }    

     return nil;

}

 

@end

說明:數組的刪除等操作處理類似,數組操作前要進行數據校驗。

2、多線程下的Crash

解決辦法:將UI更新操作放在主線程中,可以使用performSelectorOnMainThread 或 GCD

//子線程中,使用宏將更新UI的任務派發到主隊列

  #define dispatch_main_sync_safe(block) \

  if ([NSThread isMainThread]) { \

     block(); \

  } else { \

      dispatch_sync(dispatch_get_main_queue(), block); \

  }

 

  #define dispatch_async_main(block)              dispatch_async(dispatch_get_main_queue(), block)

場景2:多線程中創建單例解決辦法:使用dispatch_once,保證代碼只執行一次,保證線程安全。

//以QSAccountManager單例為例

  static QSAccountManager *_shareManager = nil;

  + (instancetype)shareManager{

 

      static dispatch_once_t once;

      dispatch_once(&once, ^{

          _shareManager = [[self alloc] init];

      });

      return _shareManager;

  }

 

  + (instancetype)allocWithZone:(struct _NSZone *)zone{

 

      static dispatch_once_t onceToken;

      dispatch_once(&onceToken, ^{

          _shareManager = [super allocWithZone:zone];

      });

      return _shareManager;

  }

 

- (nonnull id)copyWithZone:(nullable NSZone *)zone{

      return _shareManager;

  }

3、WatchDog 超時造成的Crash

4、performSelector:withObject:afterDelay下的Crash

5、SIGPIPE導致的程序退出

當伺服器close一個連接時,若client端接著發數據。根據TCP協議的規定,會收到一個RST響應,client再往這個伺服器發送數據時,系統會發出一個SIGPIPE信號給進程,告訴進程這個連接已經斷開了,不要再寫了。而根據信號的默認處理規則,SIGPIPE信號的默認執行動作是terminate(終止、退出),所以client會退出。

場景:長連接socket或重定向管道進入後臺,沒有關閉解決辦法1:切換到後臺時,關閉長連接和管道,回到前臺再重建;解決辦法2:使用signal(SIGPIPE,SIG_IGN),將SIGPIPE交給了系統處理。這麼做將SIGPIPE設為SIG_IGN,使得客戶端不執行默認動作,即不退出。

End

看完本文有收穫?請分享給更多人

關注「 iOS大全 」,提升iOS技能

相關焦點

  • iOS實錄14:淺談iOS Crash
    一、捕獲iOS Crash1、設置異常斷點並運行說明:設置Xcode異常斷點後運行程序,發生Crash時,斷點會定位到出錯的代碼行,但僅適用於開發階段。線上APP的Crash還需要通過收集Crash機制來捕獲Crash並記錄在日誌中。
  • iOS Crash 殺手排名
    殺手 NO.1:NSInvalidArgumentException 異常出現這個crash的原因有很多,選取了崩潰次數較多的crash。有3種方案可以解決該問題,如下:方案一:後臺在返回數據的時候進行校驗,對空值進行處理。但是在項目中有些空值是有特殊的用途,此種方案不可行。方案二:在轉換成NSDictionary的時候,對後臺返回的數據進行校驗,把空值轉換成NSNull對象。方案可行,但是需要對現有代碼做大的改動,每次轉換的時候都需要進行校驗,太麻煩。
  • [譯]《iOS Crash Dump Analysis》- 符號化
    為了理解所有信息是如何組合在一起的,最好從一開始開始就自己完成數據轉化任務,因此一旦我們需要診斷符號化問題,我們就已有一定的技術經驗。構建過程通常,當我們開發應用程式時,我們會將應用程式的 Debug 版本構建到我們的設備上。而當我們為測試人員、應用程式審核或應用商店構建應用時,我們構建應用程式的 Release 版本。
  • iOS Crash 分析攻略
    如下圖所示:上面兩圖來自 「深入解析 Mac OS X & iOS 作業系統」一書。如果這個version偏高,用系統的symbolicatecrash命令不能符號化日誌,一般如果看到是204, 改成104之後用symbolicatecrash就可以符號化了
  • iOS 中常見 Crash 總結
    forwardInvocation:(NSInvocation *)anInvocation {    NSLog(@"在 %@ 類中, 調用了沒有實現的類方法: %@ ",NSStringFromClass([self class]),NSStringFromSelector(anInvocation.selector));}2、儘量避免使用performSelector一系列方法
  • 深入理解iOS Crash Log
    Symbolication剛剛我們拿到的crash log的函數棧:...XCodeXCode會自動嘗試符號化Crash Log(需要文件以.crash結尾)USB連接設備打開XCode,菜單欄點Device -> Window選擇一個設備點View Device Logs然後把你的crash log,拖動到左側部分XCode會自動符號化XCode能自動符號化需要能夠找到如下文件
  • 聊聊蘋果的Bug - iOS 10 nano_free Crash
    它的crash堆棧大致為:這兩種跡象表明,這很可能是蘋果的bug。按流程,我們向蘋果提了bug report,並得到回覆:「iOS 10.2 Beta有穩定性提升」。終於等到iOS 10.2 Beta發布,我們重新統計了此類crash的系統版本分布。發現不僅在10.2 Beta正常,而且iOS 9也沒有crash。
  • iOS App 後臺 Crash 調查
    通過 PushKit 喚醒來喚醒 App 非常可靠,基本上具備 VOIP 功能的 App 都會使用這一機制來提升 App 體驗。Background Crash 調查以上是 iOS App 後臺運行的現有機制的簡單介紹,最近在調查一個 background crash,需要用到上述的後臺運行機制。用戶抱怨 App 的 cold start 非常頻繁,導致耗電嚴重。
  • iOS App 連續閃退時如何上報 crash 日誌
    crash 日誌能夠準確上報。App 無限循環 crash 時上報crash 日誌上報時,會發送網絡請求,如果請求成功之前 App 又發生 crash 該如何處理?用戶甚至會陷入無限循環的 crash 中。這篇文章介紹下出現第二種情況時,如何準確上報 crash 日誌。首先我們需要一種比較可靠的方式,可以在 app 啟動時判斷上次是否發生了啟動 crash。
  • App Crash原因及美團外賣Crash治理之路
    一、 Crash表現:程序奔潰或閃退。1、  對應用的影響            1、 程序無法繼續運行,數據丟失。            2、 糟糕的用戶體驗。1、為什麼Crash這麼重要在分享乾貨之前,王曉飛算了一筆帳:一個APP拉新成本大約是25元每人,假設3次crash會導致一個用戶的流失,那麼一年會損失2700萬,這非常驚人!從時間成本算,用戶下一單的時間最短大概是120s,遇到崩潰後,重新打開App,第二次可以成功,那麼每年三百萬用戶為千分之三的crash將損失的時間是 600天。
  • 愛奇藝 Xcrash 是怎麼捕獲 crash 的
    作者:Stan_Z連結:https://juejin.cn/post/6991356414069309477一、Xcrash簡介
  • 臉書 iOS SDK 引發大規模應用崩潰
    9to5 等媒體對此次事件做了報導• PSA: Facebook SDK bug causing several popular apps to crash right now [Update:Fixed] https://9to5mac.com/2020/05/06/psa-facebook-sdk-bug-causing-several-popular-apps-to-crash-right-now/• Facebook SDK Issue Causing Some iOS Apps to Crash https://www.macrumors.com/2020/05/
  • 手把手教你查看和分析iOS的crash崩潰異常
    如果在應用程式中接入了一些第三方的crash收集工具或者自建crash收集報告平臺的話將會很好的幫助開發者去分析和解決應用程式在線上運行的問題,當出現的崩潰問題能得到及時的解決和快速的修復時必將會大大的提升應用程式的用戶體驗。當前比較流行的crash收集分析工具很多都是基於開源的KSCrash代碼來進行封裝和改進的。
  • xCrash 詳解與源碼分析
    一、前言工欲擅其事,必先利其器。
  • 有贊crash平臺符號化實踐
    為了降低iOS App的crash率,快速排查線上crash,疑難crash的跟蹤處理,符號化崩潰日誌顯得尤為重要!一、crash日誌的收集與分析1.1 如何收集crash日誌1.手機上直接看,在隱私-分析與改進 -分析數據,可以找到所有崩潰日誌,未符號化。
  • Patch crash繞過兼容性BUG
    crash有點Windows上livekd的味道,我猜這東西是受Solaris的scat啟發,就跟SystemTap絕對受Solaris的DTrace啟發一樣。如果折騰的事涉及內核,SystemTap和crash或許都可以試試,gdb就不用強調了。
  • 有贊移動Crash平臺建設
    整個Crash上報過程、後續處理流程如下圖:為了避免crash堆棧的數據量過大,crash堆棧等長欄位存儲至HBase. Mysql中只要存儲前128預覽字符與Hbase中的row_key即可一、實現方案1.1 Crash發生時的攔截+上報Crash的攔截主要依靠各端系統的攔截機制。
  • APP閃退分析及Crash日誌獲取
    所以APP閃退可能會導致用戶的流失,所以作為研發、測試人員應該把APP的crash率降到最低。所以在測試的過程中也要特別注意閃退。說了那麼多,那麼尤其對於測試人員來說,遇到crash(或偶發)應該怎麼辦呢?
  • 深入理解Android Crash 流程
    和你一起終身學習,這裡是程式設計師Android經典好文推薦,通過閱讀本文,您將收穫以下知識點:一、一、> } else { crashTime = crashTimePersistent = null; } // Bump up the crash count of any services currently running in the proc.
  • [譯]《iOS Crash Dump Analysis》- Siri崩潰
    crash dump 為我們提供了有用的答案:Application Specific Information: objc_msgSend() selector name: didUnlockScreen:現在我們必須對崩潰的三個方面 what, where 和 when 做出一個近似的解答。