iOS 的崩潰捕獲-堆棧符號化-崩潰分析

1、獲取 Crash、dSYM 文件

獲取到的 .ips 改後綴爲 .crash 便可html

  • 真機 Crash 文件目錄:var/mobile/Library/Logs/CrashReporternode

    經過 iTunes 同步後在 macOS 目錄:~/Library/Logs/CrashReporter/MobileDevice/ios

  • 在 iOS 設備上直接查看:設置 -> 隱私 -> 分析 -> 分析數據(不一樣系統版本不同)c++

  • macOS Archives 目錄(.dSYM 和 .app):~/Library/Developer/Xcode/Archivesgit

  • 經過 iTunes Connect :Manage Your Applications -> View Details -> Crash Reportsgithub

    須要用戶在設置->隱私裏贊成共享診斷數據objective-c

    使用 bitcode 編譯成中間碼上傳的,本地不會留下 dSYM,而須要從 iTunes Connect 或者 Xcode 下載 dSYM(編譯成機器碼才能生成)算法

  • 經過 Xcode:Xcode -> Window -> Devices and Simulators -> 選中設備 -> View Device Logssql

  • 經過 Xcode 直接查看:Xcode -> Window -> Organizer -> Crashesshell

    上傳到App Store的時候,同時上傳dsym文件,那麼從Xcode中的 Crash 會自動符號化。

  • 經過 iTools -> 工具箱 -> 崩潰日誌 -> 在如下路徑查看

  • # Mac
    ~/Library/Logs/CrashReporter/MobileDevice/<DEVICE_NAME>
    # Windows
    C://Users/<USER_NAME>/AppDataRoamingApple/ComputerLogsCrashReporterMobileDevice/<DEVICE_NAME>/
    複製代碼
  • 第三方 Crash 統計庫:KSCrashplcrashReporterCrashKit

  • 第三方 Crash 統計服務:Crashlytics、Hockeyapp、友盟、Bugly

    注意:

    • 最好只集成一個 Crash 統計服務,當各家的服務都以保證本身的Crash統計正確完整爲目的時,不免出現時序手腳,強行覆蓋等等的惡意競爭。
    • 應用層參與收集 Crash日誌的服務方越多,越有可能影響iOS系統自帶的 Crash Reporter。

2、Crash 符號化(Symbolicating crash logs)

symbols 和 Symbolicate

symbols 就是函數名或變量名。符號化的過程就是把 crash log 中的內存地址轉化爲相應的函數調用關係。

通常來講,debug 模式構建的 app 會把符號表存儲在編譯好的 binary 信息中,而 release 模式構建的app會把符號表存儲在 dSYM 文件中以節省體積。

系統庫符號化文件

每當 Xcode 鏈接一臺從未在當前電腦調試過的 iOS 版本的設備時,都會花一段時間把手機的系統庫符號化文件自動導入到 ~/Library/Developer/Xcode/iOS DeviceSupport,這個過程叫 Processing symbol files。每一個系統版本的 symbols 文件約佔 2GB,因此這個文件夾會佔用很多磁盤空間。可是,最好將這些內容備份到外置硬盤,須要符號化的時候再從新拷貝回來,而不是使用清理工具清理掉。由於,系統符號化文件的獲取沒有那麼容易。

系統庫符號文件不是通用的,而是對應crash所在設備的系統版本和CPU型號的。獲取系統符號化文件的兩大方式就是經過真機,或者經過各版本 Xcode 附帶,蘋果官方沒有提供任何下載方式。有技術員總結了蒐集方式,並給出了 github 下載方式,可查看附錄。

經過 Xcode 符號化

須要3個文件,放在同一目錄下

  • crash報告(.crash文件)
  • Debug Symbol 符號文件 (.dsym文件)
  • 解壓 ipa 包後的 .app 文件

操做過程:Xcode -> Devices and Simulators -> 選中設備 -> View Device Logs

而後把 .crash文件 拖到 Device Logs 或者選擇下面的import導入.crash文件。這樣你就能夠看到crash的詳細log了。

經過命令行工具 symbolicatecrash 符號化
  • 將 .app、.dSYM、.crash 文件放到同一個目錄下。
# 找到 symbolicatecrash 工具並拷貝出來
find /Applications/Xcode.app -name symbolicatecrash -type f
# 會返回幾個路徑,拷貝其中一個
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash
 # 引入環境變量
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
# 符號解析
./symbolicatecrash appName.crash .dSYM文件路徑 > appName.log
./symbolicatecrash appName.crash appName.app > appName.log
# 將符號化的 crash log 保存在 appName.log 中
./symbolicatecrash appName.crash appName.app > appName.log
複製代碼
經過命令行工具 atos 符號化

有多個 .app、.dSYM、.crash 的時候很好用。用於符號化單個地址(可以使用腳本批量化)。

每個可執行程序都有一個build UUID來惟一標識(每次 build 都不一樣)。Crash日誌包含發生crash的這個應用(app)的 build UUID以及crash發生的時候,應用加載的全部庫文件的[build UUID]。

# 獲取 crash 文件的 UUID
grep "appName armv" *crash
# 或者
grep --after-context=2 "Binary Images:" *crash
 # 獲取 app 的 UUID
xcrun dwarfdump --uuid appName.app/appName
# 獲取 dSYM 的 UUID
xcrun dwarfdump --uuid appName.dSYM
 # 對比 app 和 crash 的 UUID 進行匹配
 # 用 atos 命令來符號化某個特定模塊加載地址 (3種方式均可以)
# 0x4000 是模塊的加載地址(必須是DWARF文件地址,而不是dSYM地址,dSYM只是一個bundle)
xcrun atos -o appName.app.dSYM/Contents/Resources/DWARF/appName -l 0x4000 -arch armv7
xcrun atos -o appName.app.dSYM/Contents/Resources/DWARF/appName -arch armv7
xcrun atos -o appName.app/appName -arch armv7
 # 另外,應用內 獲取 UUID 的方法
 #import <mach-o/ldsyms.h>
NSString *executableUUID() {
    const uint8_t *command = (const uint8_t *)(&_mh_execute_header + 1);
    for (uint32_t idx = 0; idx < _mh_execute_header.ncmds; ++idx) {
        if (((const struct load_command *)command)->cmd == LC_UUID) {
            command += sizeof(struct load_command);
            return [NSString stringWithFormat:@"%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X",
                    command[0], command[1], command[2], command[3],
                    command[4], command[5],
                    command[6], command[7],
                    command[8], command[9],
                    command[10], command[11], command[12], command[13], command[14], command[15]];
        } else {
            command += ((const struct load_command *)command)->cmdsize;
        }
    }
    return nil;
}
 # 經過 iTunes Connect 網站來下載 dSYM 的話,對下載下來的每一個 dSYM 文件都執行一次
xcrun dsymutil -symbol-map ~/Library/Developer/Xcode/Archives/[...]/BCSymbolMaps [UUID].dSYM
複製代碼

示例:

# 有兩行未符號化的 crash log
* 3 appName 0x000f462a 0x4000 + 984618 
* 4 appName **0x00352aee** 0x4000 + 3468014
 # 1. 執行
xcrun atos -o appName.app.dSYM/Contents/Resources/DWARF/appName -l 0x4000 -arch armv7
# 2. 而後輸入 0x00352aee
# 3. 符號化結果:
-[UIScrollView(UITouch) touchesEnded:withEvent:] (in appName) (UIScrollView+UITouch.h:26)
複製代碼
注意:
  • 使用 symbolicatecrash,先拷貝出 symbolicatecrash 文件比較方便。
  • 不管是 symbolicatecrash 仍是 atos,都只須要.crash和 .dSYM ,或 .crash和 .app,就能夠符號化了。
  • 系統方法的堆棧符號化須要系統符號化文件,若是本地 macOS 沒有該文件,也沒有該版本 iOS 設備可拷貝,可經過 Github iOS-System-Symbols(iOS 各版本系統符號庫) 下載,

3、Crash 文件結構

1. Process Information(進程信息)
Incident Idnetifier 崩潰報告的惟一標識符,不一樣的Crash
CrashReporter Key 設備的 id(不是 uuid)。一般同一個設備上同一版本的 app 發生Crash時,該值都是同樣的。
Hardware Model 設備類型
Process 進程名稱[進程 id],進程一般是 app 名字
Path 可執行程序的位置
Identifier com.companyName.appName
Version app 版本號
Code Type CPU 架構
Parent Process 父進程,iOS中App一般都是單進程的,通常父進程都是 launchd
2. Basic Information(基本信息)
Date/Time Crash發生的時間,可讀的字符串
OS Version 系統版本(build 號)
Report Version Crash日誌的格式,目前基本上都是104,不一樣的version裏面包含的字段可能有不一樣
3. Exception(異常)
Exception Type 異常類型
Exception Subtype: 異常子類型
Crashed Thread 發生異常的線程號
Exception Information 額外診斷信息

從macOS Sierra, iOS 10, watchOS 3, 和 tvOS 10開始,額外診斷信息,包括:

  1. 應用的具體信息:在進程被終止前捕捉到的框架錯誤信息

  2. 內核信息:關於代碼簽名問題的細節

  3. Dyld (動態連接庫)錯誤信息:被動態連接器提交的錯誤信息

# 一段由於找不到連接庫而致使進程被終止的Crash Report的摘錄
Dyld Error Message:

Dyld Message: Library not loaded: @rpath/MyCustomFramework.framework/MyCustomFramework

 Referenced from: /private/var/containers/Bundle/Application/CD9DB546-A449-41A4-A08B-87E57EE11354/TheElements.app/TheElements  
 Reason: no suitable image found.
 # 一段由於沒能快速加載初始view controller而致使進程被終止的Crash Report的摘錄
Application Specific Information:
com.example.apple-samplecode.TheElements failed to scene-create after 19.81s (launch took 0.19s of total time limit 20.00s)
Elapsed total CPU time (seconds): 7.690 (user 7.690, system 0.000), 19% CPU
Elapsed application CPU time (seconds): 0.697, 2% CPU
複製代碼
4. Thread Backtrace(線程回溯)

Crash 發生時的線程的調用棧,沒有符號化前是內存地址。

5. Thread State(線程狀態)

Crash 發生時的寄存器狀態。在你讀一個 Crash Report 的時候,瞭解線程狀態並不是必須,可是若是你想更好地瞭解crash的細節,這會起一些幫助,這須要一些處理器硬件只是和彙編知識的儲備。

LLDB與彙編調試-提升你的調試效率

6. Binary Images(二進制映像)

Crash 發生時 app 可執行文件、加載的全部系統庫和第三方庫。

 # app 可執行文件 Elephant
 0x104e80000 -        0x107b2bfff +Elephant arm64  <38c058044caa34818a83d88981986fad> /var/containers/Bundle/Application/5694FC83-018E-46E7-B060-A008867D3C9D/Elephant.app/Elephant
 # WCDB 可執行文件。b512f6d343e73a0db1bcb499d2597c8a 是 WCDB 的 UUID
 # 符號化時 dsym 的 UUID 須要與之匹配才能符號化
 0x10b724000 -        0x10b86ffff  WCDB arm64  <b512f6d343e73a0db1bcb499d2597c8a> /private/var/containers/Bundle/Application/5694FC83-018E-46E7-B060-A008867D3C9D/Elephant.app/Frameworks/WCDB.framework/WCDB
複製代碼

4、Crash 的類型

4.1 兩類主要的 Crash

引起崩潰的代碼本質上就兩類,

一類是 c/c++ 語言層面的錯誤,好比野指針,除零,內存訪問異常等等(相對複雜)

對於前者,不管是 iOS 仍是 Android 系統,其底層都是 unix 或者是類 unix 系統,均可以經過信號機制來獲取 signal 或者是 sigaction (可是隻能捕捉有限的幾種類型),設置一個回調函數。

  • Watchdog 超時、用戶強制退出、低內存終止等,系統拋出Unix信號,沒有任何的錯誤堆棧信息

另外一類是未捕獲異常 Uncaught Exception(相對簡單)

iOS 下面最多見的就是 Objective-C 的NSException(@throw 拋出),可使用NSUncaughtExceptionHandler catch 住防止崩潰。

  • 如數組越界,給對象發送了沒法識別的消息(selector方法沒有實現,對象調用方法出錯)等,系統拋出一個NSException對象,對象中有出錯的堆棧,描述了出錯的代碼位置、類名和方法名
4.1.1 Bus Error
  • Non-existent address(訪問不存在的內存地址)
  • Unaligned access(訪問未對齊的內存地址)
  • Paging errors(分頁錯誤)

在檢測順序上,先檢測 SIGBUS,再檢測 SIGSEGV。

SIGBUS 地址被放到地址總線以後,檢測出地址不對齊,發出異常信號,

SIGSEGV 地址已經放到地址總線上後,在後續流程中檢測出內存違法訪問,發出異常信號。

  • SIGBUS (Bus error)訪問非法地址

    指針所對應的地址是有效地址,但總線不能正常使用該指針,一般是未對齊的數據訪問所致。

    一些處理器架構上要求對齊訪問數據,好比只能從4字節邊界上讀取一個4字節的數據類型(對於長度4個字節的對象,其存放地址起碼要被4整除才能夠)。不然向當前進程分發SIGBUS信號。

  • SIGSEGV (Segmentation fault、segfault)合法地址的非法訪問

    在 ARC 後不多遇到,意味着指針所對應的地址是無效地址,沒有物理內存對應該地址。

    訪問不屬於本進程的內存地址

    往沒有寫權限的內存地址寫數據

    訪問已被釋放的內存

  • SEGV(Segmentation  Violation)

    表明無效內存地址,好比空指針,未初始化指針,棧溢出等。

4.1.2 其餘異常類型
  • EXC_CRASH(SIGABRT)

    情形:Abnormal Exit 異常退出

    未捕獲的 Objective-C 異常(NSException),致使系統發送了 Abort 信號退出,致使這類異常崩潰的緣由是捕獲到 Objective-C/C++ 異常,而且調用了 abort() 函數,會在斷言/app內部/操做系統用終止方法拋出。

    1. 一般發生在異步執行系統方法的時候,如CoreData/NSUserDefaults等,還有一些其餘的系統多線程操做。這並不必定意味着是系統代碼存在bug,代碼僅僅是成了無效狀態,或者異常狀態。
    2. 一般Foundation庫中的容器爲了保護狀態正常會作一些檢測,例如插入nil到數組中等會遇到此類錯誤。
    3. App Extensions,例如輸入法,若是花了太多時間作初始化的話就會以這種異常退出(看門狗機制)。若是擴展程序因爲在啓動時掛起進而被kill掉,那 Report 中的Exception Subtype字段會寫 LAUNCH_HANG。由於擴展App沒有main函數,因此任何狀況下的在static constructors和+load方法裏的初始化時間都會體如今你的擴展或者依賴庫中。所以你應當儘量的推遲這些邏輯。
    example 1: unrecognized selector sent to instance
    example 1: attempt to insert nil object from objects
    複製代碼

    對於可能在別處被釋放的對象,要本身持有一份(alloc 或 copy)。

  • EXC_BREAKPOINT(SIGTRAP)

    情形:Trace Trap 追蹤捕獲

    和進程異常退出相似,這種異常是因爲在特殊的節點加入debugger調試節點,若是當前沒有調試器(debugger)依附,那麼則會致使進程被殺掉。能夠經過 __builtin_trap() 在代碼裏手動出發這種異常。

    1. 這種 Crash 在 iOS 底層的框架中常常出現,最多見的是GCD。底層庫(例如libdispatch)會在遇到fatal錯誤的時候陷入這個困局。

    2. Swift代碼會在運行時的時候遇到下述問題時拋出這種異常:

      一個non-optional的類型被賦予一個nil值

      一個失敗的強制轉換

    遇到這種錯誤,查下堆棧信息並想清楚是在哪裏遇到了未知狀況(unexpected condition)。額外信息也可能會在設備的控制檯的日誌裏出現。你應當儘可能修改你的代碼,去優雅的處理這種運行時錯誤。例如,處理一個optional的值,經過可選綁定(Optional binding)而不是強制解包來得到其值。

  • EXC_BAD_INSTRUCTION(SIGILL)

    情形:Illegal Instruction 非法指令

    當嘗試去執行一個非法或者未定義的指令時會觸發該異常。有多是由於線程在一個配置錯誤的函數指針的誤導下嘗試jump到一個無效地址。 在Intel處理器上,ud2操做碼會致使一個EXC_BAD_INSTRUCTION異常,可是這個一般用來作調試用途。

    在Intel處理器上,Swift會在運行時碰到未知狀況時被中止。 詳情參考Trace Trap。

  • SIGKILL

    情形:Killed

    進程收到系統指令被幹掉。請自行查看Termination Reason(會包含一個命名空間和代碼)來定位線程被幹掉的緣由。

  • SIGQUIT

    情形:Quit 退出

    這個異常是因爲其它進程擁有高優先級且能夠管理本進程(所以被高優先級進程Kill掉)所致使。SIGQUIT不表明進程發生Crash了,可是它確實反映了某種不合理的行爲。

    iOS中,若是佔用了太長時間,鍵盤擴展程序會隨着宿主app被幹掉。所以,這種狀況的異常下不太可能會在Crash Report中出現合理可讀的異常代碼。大機率是由於一些其它代碼在啓動時佔用了太長時間可是在總時間限制前(看門狗的時間限制,見上文中的表格)成功結束了,可是執行邏輯在extension退出的時候被錯誤的執行了。你應該運行Profile,仔細分析一下extension的各部分消耗時間,把耗時較多的邏輯放到background或者推遲(推遲到extension加載完畢)。

  • EXC_ARITHMETIC

    除零錯誤會拋出此類異常

    arithmetic [ə'rɪθmətɪk] 算術,算法

  • SIGPIPE 管道破裂

    這個信號一般在進程間通訊產生,好比採用FIFO(管道)通訊的兩個進程,讀管道沒打開或者意外終止就往管道寫,寫進程會收到SIGPIPE信號。

    此外用Socket通訊的兩個進程,寫進程在寫Socket的時候,讀進程已經終止。

    對一個端已經關閉的socket調用兩次寫入操做,第二次寫入將會產生SIGPIPE信號,該信號默認結束進程。

    // 預防方式,寫在 pch 文件
    // 僅在 iOS 系統上支持 SO_NOSIGPIPE
    #if defined(SO_NOSIGPIPE) && !defined(MSG_NOSIGNAL)
        // We do not want SIGPIPE if writing to socket.
        const int value = 1;
        setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &value, sizeof(int));
    #endif
    複製代碼
    另一些異常類型

    爲了防止一個應用佔用過多的系統資源,設計了 watchdog 的機制, watchdog 會監測應用的性能。若是超出了該場景所規定的運行時間,watchdog 強制終結這個應用的進程。

    Exception Code 說明
    0xbaaaaaad 並不是一個真正的Crash,由用戶同時按Home鍵和音量鍵觸發。
    0xbad22222 當VoIP程序在後臺太過頻繁的激活時,系統可能會終止此類程序。
    0x8badf00d(badfood) launch/resume/suspend/quit/background 響應超過規定時間會被 Watchdog 終止(詳見下表), 併產生一個崩潰日誌。在鏈接Xcode調試時爲了便於調試,系統會暫時禁用掉Watchdog,因此此類問題的發現須要使用正常的啓動模式。
    0xc00010ff 程序執行大量耗費CPU和GPU的運算,致使設備過熱,觸發系統過熱保護被系統終止。這個也許是和發生crash的特定設備有關,或者是和它所在的環境有關。
    0xdead10cc(deadlock) 程序退到後臺時還佔用系統資源(如通信錄)被系統終止。或者程序掛起時拿到了文件鎖或者sqlite數據庫所長期不釋放直到被凍結。若是你的app在掛起時拿到了文件鎖或者sqlite數據庫鎖,它必須請求額外的後臺執行時間(request additional background execution time )並在被掛起前完成解鎖操做。
    0xdeadfa11(deadfall) 程序無響應用戶強制退出。當用戶長按電源鍵,直到屏幕出現關機確認畫面後再長按Home鍵,將強制退出應用。(不是雙擊 home 的強退)Exception Note 會有 SIMULATED 字段
    0x2bad45ec app由於安全違規操做被iOS系統終止。終止描述會寫:「進程被查到在安全模式進行非安全操做」,暗示app嘗試在禁止屏幕繪製的時候繪製屏幕,例如當屏幕鎖定時。用戶可能會忽略這種異常,尤爲當屏幕是關閉的或者當這種終止發生時正好鎖屏。

    說明:經過App Switcher(就是雙擊home鍵出現的那個界面)並不會生成Crash Report。一旦app進入掛起狀態,被iOS在任什麼時候間終止掉都是合理的,所以這時候不會生成Crash Report。

    如下異常代碼只針對 watchOS

    Exception Code 說明
    0xc51bad01 在後臺任務佔用了過多的cpu時間而致使watch app被幹掉。想要解決這個問題,優化後臺任務,提升CPU執行效率,或者減小後臺的任務運行數量。
    0xc51bad02 在後臺的規定時間內沒有完成指定的後臺任務而致使watch app被幹掉。想要解決這個問題,須要當app在後臺運行時減小app的處理任務。
    0xc51bad03 沒有在規定時間內完成後臺任務,且系統一直很是忙以致於app沒法獲取足夠的CPU時間來完成後臺任務。雖然一個app能夠經過減小自身在後臺的運行任務來避免這個問題,可是0xc51bad03這個錯誤把矛頭指向了太高的系統負載,而非app自己有什麼問題。
  • 附:Watchdog 超時時間

    場景 超時時間
    launch(啓動) 20s
    resume(恢復) 10s
    suspend(掛起) 10s
    quit(退出) 6s
    background(後臺) 10min

    簡單說,就是如下代理必須在規定時間內執行完畢,讓程序響應起來。

    - (void)applicationDidFinishLaunching:(UIApplication *)application;
    - (void)applicationDidBecomeActive:(UIApplication *)application;
    - (void)applicationWillResignActive:(UIApplication *)application;
    - (void)applicationDidEnterBackground:(UIApplication *)application;
    - (void)applicationWillEnterForeground:(UIApplication *)application;
    - (void)applicationWillTerminate:(UIApplication *)application;
    複製代碼

崩潰(準確的說是程序異常終止)是程序接收到未處理信號的結果。

未處理信號有三個來源:內核、其餘進程和應用自己。致使崩潰最多見的兩個信號以下:

  • EXC_BAD_ACCESS 是一種由內核發出的Mach異常,一般是由於應用試圖訪問不存在的內存空間致使的。若是未能在Mach內核級別進行處理,它將被轉化爲SIGBUS或者SIGSEGV BSD信號。
  • SIGABRT是當產生未捕獲的NSException或者obj_exception_throw時,應用發給自身的BSD信號。

在 Objective-C 異常中,致使異常拋出最多見的緣由是應用向對象發送了未實現的方法選擇器(好比拼寫錯誤,對象混淆或者向已經釋放的對象發送消息)。

4.2 Low Memory Report 低內存報告

Low Memory Termination

跟通常的Crash結構不太同樣,一般有Free pages,Wired Pages,Purgeable pages,largest process 組成,同時會列出當前時刻系統運行全部進程的信息。

Low Memory Report 與其它 Crash Report 不一樣,它沒有堆棧信息,因此不須要符號化。一個低內存 Report的Header會和 Crash Report 的header有些相似。緊接着Header的時各個字段的系統級別的內存統計信息。記錄下頁大小(Page Size)字段。每個進程的內存佔用大小是根據內存的頁的數量來 Report的。一個低內存 Report最重要的部分是進程表格。這個表格列出了全部的運行進程,包括系統在生成低內存 Report時的守護進程。若是一個進程被」遺棄」了,會在[緣由]一列附上具體的緣由。一個進程可能被遺棄的緣由有:

  • [per-process-limit]

    進程佔用超過了它的最大內存值。每個進程在常駐內存上的限制是早已經由系統爲每一個應用分配好了的。超過這個限制會致使進程被系統幹掉。

    注意:擴展程序(nimo: Extension app, 例如輸入法等)的最大內存值更少。一些技術,例如地圖視圖和SpriteKit,佔用很是多的基礎內存,所以不適合用在擴展程序裏。

  • [vm-pageshortage]/[vm-thrashing]/[vm]

    因爲系統內存壓力被幹掉。

  • [vnode-limit]

    打開太多文件了。

    注意:系統會盡可能避免在vnodes已經枯竭的時候幹掉高頻app。所以你的應用若是在後臺,即使並無佔用什麼vnode,而有可能被殺掉。

  • [highwater]

    一個系統守護進程超過過了它的內存佔用高水位(就是已經很危險了)。

  • [jettisoned]

    進程由於其它不可描述的緣由被殺掉。

當你發現一個低內存crash,與其去擔憂哪一部分的代碼出現問題,還不如仔細審視一下本身的內存使用習慣和針對低內存告警(low-memory warning)的處理措施。Locating Memory Issues in Your App 列出瞭如何使用Leaks Instrument工具來檢查內存泄漏,和如何使用Allocations Instrument的Mark Heap 功能來避免內存浪費。 Memory Usage Performance Guidelines 討論瞭如何處理接受到低內存告警的問題,以及如何高效使用內存。固然,也推薦你去看下2010年的WWDC中的 Advanced Memory Analysis with Instruments 那一章節。

**重要:**Leaks和Allocation工具不能檢測全部的內存使用狀況。你須要和VM Tracker工具一塊兒運行(包含在Allocation工具裏)來查看你的內存運行。默認VM Tracker是不可用的。若是想經過VM Tracker來profile你的應用,點擊instrument工具,選中」Automatic Snapshotting」標籤或者手動點擊」Snapshot Now」按鈕。

5、Crash 的捕獲

5.0 Last Exception Backtrace

若程序因 NSException 而 Crash,系統日誌中的 Last Exception Backtrace 信息是完整準確的,不會受應用層的 Crash 統計服務影響,可做爲排查問題的參考線索。若是 Last Exception Backtrace,只包含16進制信息的日誌,必須進行符號化來獲取有價值的堆棧信息

# 未符號化的異常堆棧
Last Exception Backtrace:

(0x18eee41c0 0x18d91c55c 0x18eee3e88 0x18f8ea1a0 0x195013fe4 0x1951acf20 0x18ee03dc4 0x1951ab8f4 0x195458128 0x19545fa20 0x19545fc7c 0x19545ff70 0x194de4594 0x194e94e8c 0x194f47d8c 0x194f39b40 0x194ca92ac 0x18ee917dc 0x18ee8f40c 0x18ee8f89c 0x18edbe048 0x19083f198 0x194d21bd0 0x194d1c908 0x1000ad45c 0x18dda05b8)
複製代碼

5.1 處理未捕獲異常(uncaught exceptions)

Demo :github.com/xcysuccess/…

有兩種方式能夠捕獲那些會致使崩潰的未捕獲狀態。

  • 使用 NSUncaughtExceptionHandler 函數來安裝未捕獲 Objective-C 異常的處理器。
  • 使用 signal 函數來安裝 BSD 信號處理器。

注意:signal 要在沒有附加 debugger 的環境下獲取,不然會被 debugger 優先攔截。UncaughtExceptionHandler能夠在調試狀態下捕獲

抓取 NSException
// 安裝 Objective-C 異常處理器和信號處理的代碼以下:
void InstallUncaughtExceptionHandler() {
	NSSetUncaughtExceptionHandler(&MyUncaughtExceptionHandler);
	signal(SIGABRT, SignalHandler);
	signal(SIGILL, SignalHandler);
	signal(SIGSEGV, SignalHandler);
	signal(SIGFPE, SignalHandler);
	signal(SIGBUS, SignalHandler);
	signal(SIGPIPE, SignalHandler);
}
// 對於異常和信號的響應會在 MyUncaughtExceptionHandler 和 SignalHandler 中實現。在樣例程序中,以上兩者的處理方式相同。

void MyUncaughtExceptionHandler(NSException *exception) {
    NSString *ret = [NSString stringWithFormat:@"異常名稱:\n%@\n\n異常緣由:\n%@\n\n出錯堆棧內容:\n%@\n",exception.name, exception.reason, exception.callStackSymbols];
    // 將捕獲到的 exception 細節上傳到後臺
}
複製代碼
抓取 Signal

signal信號是Unix系統中的,是一種異步通知機制.信號傳遞給進程後,在沒有處理函數的狀況下,程序能夠指定三種行爲:

  1. 忽略該信號,可是對於信號SIGKILLSIGSTOP不可忽略
  2. 使用默認的處理函數SIG_DFL(即 signal(sig, SIG_DFL);),大多數信號的默認動做是終止進程
  3. 捕獲信號,執行用戶定義的函數

有兩個特殊的常量:

  • SIG_IGN,向內核表示忽略此信號.對於不能忽略的兩個信號SIGKILLSIGSTOP,調用時會報錯
  • SIG_DFL,執行該信號的系統默認動做.

還有兩個經常使用的函數

  • int kill(pid_t pid, int signo);,發送信號到指定的進程
  • int raise(int signo);,發送信號給本身.
// UNIX系統中經常使用的信號有如下幾種:
SIGABRT--程序停止命令停止信號 
SIGBUS--程序內存字節未對齊停止信號
SIGFPE--程序浮點異常信號
SIGILL--程序非法指令信號
SIGSEGV--程序無效內存停止信號
SIGTERM--程序kill停止信號
SIGKILL--程序結束接收停止信號 
    
SIGALRM--程序超時信號 
SIGHUP--程序終端停止信號
SIGINT--程序鍵盤中斷信號 
SIGSTOP--程序鍵盤停止信號  
SIGPIPE--程序Socket發送失敗停止信號

// 抓取的是如下幾種
static int Beacon_errorSignals[] = {
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGSEGV,
    SIGTRAP,
    SIGTERM,
    SIGKILL,
};
for (int i = 0; i < Beacon_errorSignalsNum; i++) {
    signal(Beacon_errorSignals[i], &mysighandler);
}
// 抓取信號的處理函數
void mysighandler(int sig) {
    void* callstack[128];
    NSString* name ;
    int i, frames = backtrace(callstack, 128);
    for (i = 0; i < Beacon_errorSignalsNum; i++) {
        if (Beacon_errorSignals[i] == sig ) {
            name = [Beacon_errorSignalNames[i] copy];
            break;
        }
    }
    char** strs = backtrace_symbols(callstack, frames);
    NSMutableString* exceptionStr = [[NSMutableString alloc]initWithFormat:@"異常名稱:\n%@\n\n出錯堆棧內容:\n",name];
    for (i =0; i <frames; i++) {
        [exceptionStr appendFormat:@"%s\n",strs[i]];
    }
    free(strs);
}

// 在應用崩潰後,保持運行狀態而不退出,讓響應更加友好
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);

while (!dismissed) {
	for (NSString *mode in (__bridge NSArray *)allModes) {
		CFRunLoopRunInMode((__bridge CFStringRef)mode, 0.001, false);
	}
}

CFRelease(allModes);
複製代碼

這裏只處理最多見的信號,可是,你能夠爲本身的程序添加所需的全部異常信號。

注意,有兩種異常是不能捕獲的:SIGKILL和SIGSTOP。它們會終止或者暫停應用。(SIGKILL是命令行函數kill -9發出的,SIGSTOP是鍵入Control-Z發出的)。

若是你發現本應該被捕捉的異常並無被捕捉到,請肯定您沒有在building應用或者library時添加了-no_compact_unwind標籤。

64位 iOS 用了zero-cost的異常實現機制。在zero-cost系統裏,每個函數都有一個額外的數據,它會描述若是一個異常在跨函數範圍內實現,該如何展開相應的堆棧信息。若是一個異常發生在多個堆棧可是沒有可展開的數據,那麼異常處理函數天然沒法跟蹤並記錄。也許在堆棧很上層的地方有異常處理函數,可是若是那裏沒有一個片斷的可展開信息,沒辦法從發生異常的地方到那裏。指定了-no_compact_unwind標籤代表你那些代碼沒有可展開信息,因此你不能跨越函數拋出異常(也就是說沒法經過別的函數捕捉當前函數的異常)。

5.2 Xcode 提供的調試工具

都在 Edit Scheme -> Diagnostics(診斷) 依次能夠找到

Runtime Sanitization
  • Address Sanitizer(地址消毒劑)

    AddressSanitizer的原理是當程序建立變量分配一段內存時,將此內存後面的一段內存也凍結住,標識爲中毒內存。當程序訪問到中毒內存時(越界訪問),就會拋出異常,並打印出相應log信息。調試者能夠根據中斷位置和的log信息,識別bug。若是變量釋放了,變量所佔的內存也會標識爲中毒內存,這時候訪問這段內存一樣會拋出異常(訪問已經釋放的對象)。

  • Thread Sanitizer

    用於解決多線程問題:如何用Xcode8解決多線程問題

    • Use of uninitialized mutexes(使用未初始化的互斥器)
    • Thread leaks (missing pthread_join) 線程泄漏(缺乏pthread_join)
    • Unsafe calls in signal handlers (ex:malloc) 信號處理程序中的不安全調用(例如:malloc)
    • Unlock from wrong thread 從錯誤的線程解鎖
    • Data races 數據競爭(只要涉及到多線程編程,遇到的機率很是之高,寫多線程代碼時最容易遇到的問題,一旦踩坑,現象每每是偶現的,難以調試)

    大體原理是記錄每一個線程訪問變量的信息來作分析,值得一提的是,現階段的Thread Sanitizer最多隻同時記錄4個線程的訪問信息,在複雜的場景下,可能出現偶爾檢測不出data race的場景,因此須要長時間常常性的運行來儘量多的發現data race,這也是爲何蘋果建議默認開啓Thread Sanitizer,並且Thread Sanitizer 形成的額外性能損耗很是之小。

    Thread Sanitizer 現階段只能在模擬器環境下執行,真機還不支持,有同窗測試發現,只支持64位系統,也就是說iPhone 5及其更早的模擬器也不支持,iPhone 5s 以後纔是64位系統。

Memory Management
  • Malloc Scribble

    申請內存後在申請的內存上填 0xAA,內存釋放後在釋放的內存上填 0x55;再就是說若是內存未被初始化就被訪問,或者釋放後被訪問,就會引起異常,這樣就可使問題儘快暴漏出來。

    Scribble 實際上是 malloc 庫 libsystem_malloc.dylib 自身提供的調試方案

  • Malloc Guard Edges

    申請大片內存的時候在先後page上加保護,詳見保護模式

  • Guard Malloc

    使用 libgmalloc 捕獲常見的內存問題,好比越界、釋放以後繼續使用。

    因爲 libgmalloc 在真機上不存在,所以這個功能只能在模擬器上使用.

  • Zombie Objects(殭屍對象)

    Instrument 也有一個 Zombie 工具,使用起來差很少。

    Zombie 的原理是用生成殭屍對象來替換 dealloc 的實現,當對象引用計數爲 0 的時候,將須要 dealloc 的對象轉化爲殭屍對象。若是以後再給這個殭屍對象發消息,則拋出異常,並打印出相應的信息,調試者能夠很輕鬆的找到異常發生位置。

    若是 objc_msgSend 或者 objc_release出如今crash的線程的附近,則進程有可能嘗試去給一個被釋放的對象發送消息,那麼可以使用 Zombie 調試

    # 控制檯會多一些調試信息
    message sent to deallocated instance 0x60800000c380
    複製代碼
Analyze(靜態代碼分析)

不是那麼準確,可是會發現一些問題

能夠發現編譯中的 warning,內存泄漏隱患,甚至還能夠檢查出邏輯上的問題;因此在自測階段必定要解決Analyze發現的問題,能夠避免出現嚴重的bug。

主要分析如下四種問題:

  • 邏輯錯誤:訪問空指針或未初始化的變量等;
  • 內存管理錯誤:如內存泄漏等;
  • 聲明錯誤:從未使用過的變量;
  • 調用錯誤:未包含使用的庫和框架。
# 內存泄漏隱患
Potential(潛在) Leak of an object allocated on line ……
# 數據賦值隱患
The left operand of …… is a garbage value;
# 對象引用隱患
Reference-Counted object is used after it is released;
複製代碼
Profile(就是運行 Instrument)

真正運行程序,對程序進行內存分析(查看內存分配狀況、內存泄露)

優勢:分析很是準確,若是發現有提示內存泄露,基本能夠判定代碼問題

缺點:分析效率低(真正運行了一段代碼,才能對該代碼進行內存分析)

6、附錄

相關文章
相關標籤/搜索