iOS Crash的捕獲知識

weixin_33797791發表於2018-12-14

1. crash的型別

crash一般產生自 iOS 的微核心 Mach,然後在 BSD 層轉換成 UNIX SIGABRT 訊號,以標準 POSIX 訊號的形式提供給使用者。NSException 是使用者在處理 App 邏輯時,用程式設計的方法丟擲。

  • Mach 異常:EXC_CRASH
  • UNIX 訊號:SIGABRT
  • NSException 異常:應用層,通過 NSUncaughtExceptionHandler 捕獲

2. crash的捕獲的方式

  • Mach 異常捕獲。如果想要做mach 異常捕獲,需要註冊一個異常埠,這個異常埠會對當前任務的所有執行緒有效,如果想要針對單個執行緒,可以通過 thread_set_exception_ports註冊自己的異常埠,發生異常時,首先會將異常拋給執行緒的異常埠,然後嘗試拋給任務的異常埠,當我們捕獲異常時,就可以做一些自己的工作,比如,當前堆疊收集等。

  • Unix 訊號捕獲。對於Mach 異常,作業系統會將其轉換為對應的 Unix訊號,所以如果你對Mach不熟悉的話,也可以通過註冊signalHandler的方式來做訊號異常。

     signal(SIGABRT, SignalExceptionHandler)   
     signal(SIGSEGV, SignalExceptionHandler)
     signal(SIGBUS, SignalExceptionHandler)
     signal(SIGTRAP, SignalExceptionHandler)
     signal(SIGILL, SignalExceptionHandler)
    
  • NSException 捕獲
    對於NSException異常,也比較容易處理,通過註冊NSUncaughtExceptionHandler捕獲異常資訊即可,將拿到的NSException細節寫入Crash日誌,上傳到後臺做資料分析

NSSetUncaughtExceptionHandler(UncaughtExceptionHandler)

多個 Crash 收集框架存在時,往往會存在衝突。
不管是對於 Signal 捕獲還是 NSException 捕獲都會存在 handler 覆蓋的問題,正確的做法應該是先判斷是否有前者已經註冊了 handler,如果有則應該把這個 handler 儲存下來,在自己處理完自己的 handler 之後,再把這個 handler 丟擲去,供前面的註冊者處理。

資料: 漫談iOS Crash收集框架

4.堆疊符號解析

堆疊符號化還原有三種常見的方法:

  • symbolicatecrash
  • mac 下的 atos 工具
  • 通過 dSYM 檔案提取地址和符號的對應關係,進行符號還原
// 未符號化前
Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   libobjc.A.dylib                 0x000000018b816f30 0x18b7fc000 + 110384 (objc_msgSend + 16)
1   UIKit                           0x0000000192e0a79c 0x192c05000 + 2119580 (<redacted> + 72)
2   UIKit                           0x0000000192c4db48 0x192c05000 + 297800 (<redacted> + 312)
3   UIKit                           0x0000000192c4d988 0x192c05000 + 297352 (<redacted> + 160)
4   QuartzCore                      0x00000001900d6404 0x18ffc5000 + 1119236 (<redacted> + 260)

// 符號化後
Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   libobjc.A.dylib                 0x000000018b816f30 objc_msgSend + 16
1   UIKit                           0x0000000192e0a79c -[UISearchDisplayController _sendDelegateDidBeginDidEndSearch] + 72
2   UIKit                           0x0000000192c4db48 -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 312
3   UIKit                           0x0000000192c4d988 -[UIViewAnimationState animationDidStop:finished:] + 160
4   QuartzCore                      0x00000001900d6404 CA::Layer::run_animation_callbacks(void*) + 260

Xcode的Organizer內建了symbolicatecrash,所以我們才可以直接看到符號化的崩潰堆疊日誌

資料:實戰iOS崩潰堆疊的符號化解析
輕量級的堆疊採集框架:BSBacktraceLogger

5.啟動連續閃退

  • 閃退原因

      1. 資料庫損壞:在日常使用如異常退出、斷電,或者錯誤的操作(參考:sqlite corruption causes)。
      2. 檔案損壞:處理檔案時如果沒有 @try...catch,損壞檔案會丟擲 NSException 導致 crash
      3. 網路返回資料處理異常:比如預期返回陣列,但實際返回了字典,對字典物件執行 -objectAtIndex 方法會產生 crash: unknow selector send to object;,或返回破損的 Tar 包,在解壓失敗導致 crash。
      4. 程式碼 bug:當必 crash 的程式碼出現在啟動關鍵路徑中,就會導致連續閃退。
      5. 針對 1,可以通過工具修復資料庫,或者刪除 DB。針對2,可以刪除檔案來進行修復。對於 3 和 4,我們需要具體地分析 crash 案例,通過 JSPatch 來進行修復。
    
  • 計時器方法

    1. 維護一個計數變數,用於表示連續閃退的次數
    2. 在啟動 application:didFinishLaunchingWithOptions: 後使計數加一
    3. 接著使用 dispatch_after 方法在 5s 後清零計數,如果 App 活不過 5 秒計數就不會被清零
    4. 如果發現計數變數 > n,表明 App 連續 n 次連續閃退,啟動保護流程,重置計數。
    5. 當保護流程完成後,進入 App 正常啟動流程
    
  • 時間陣列比對

在本地儲存一個 App 每次啟動時間、閃退時間、手動關閉時間的時間陣列,然後在 App 啟動時根據分析各個時間戳判斷是否存在連續閃退(當閃退時間減去啟動時間小於閾值 5 秒時,則認為是啟動閃退)

1. App 每次啟動時,記錄當前時間 launchTs,寫入時間陣列;
2. App 每次啟動時,通過 crash 採集庫,獲取上次 crash report 的時間戳 crashTs,寫入時間陣列;
3. App 在接收到 UIApplicationWillTerminateNotification 通知時,記錄當前時間戳 terminateTs,寫入時間陣列。注意,之所以要記錄 terminateTs,是為了排除一種特殊情況,即使用者啟動 App 之後立即手動 kill app。

資料:
iOS 啟動連續閃退保護方案
連續啟動 crash 自修復技術實現與原理解析
兩種 App 啟動連續閃退檢測策略

6.crash收集方式

  • 第三方平臺:Fabric、友盟、騰訊 Bugly等,資料會上傳到這些平臺
  • 第三方工具: KSCrash、plcrashreporter等,可自行處理收集的crash(傳送到郵箱/上傳自己伺服器)
  • 自定義捕獲+堆疊符號化

相關文章