一嚷量、獲取 Crash帮孔、dSYM 文件
獲取到的 .ips 改后綴為 .crash 即可
-
真機(jī) Crash 文件目錄:var/mobile/Library/Logs/CrashReporter
通過 iTunes 同步后在 macOS 目錄:~/Library/Logs/CrashReporter/MobileDevice/
在 iOS 設(shè)備上直接查看:設(shè)置 -> 隱私 -> 分析 -> 分析數(shù)據(jù)(不同系統(tǒng)版本不一樣)
macOS Archives 目錄(.dSYM 和 .app):~/Library/Developer/Xcode/Archives
-
通過 iTunes Connect :Manage Your Applications -> View Details -> Crash Reports
需要用戶在設(shè)置->隱私里同意共享診斷數(shù)據(jù)
使用 bitcode 編譯成中間碼上傳的,本地不會留下 dSYM,而需要從 iTunes Connect 或者 Xcode 下載 dSYM(編譯成機(jī)器碼才能生成)
通過 Xcode:Xcode -> Window -> Devices and Simulators -> 選中設(shè)備 -> View Device Logs
-
通過 Xcode 直接查看:Xcode -> Window -> Organizer -> Crashes
上傳到App Store的時候,同時上傳dsym文件滴铅,那么從Xcode中的 Crash 會自動符號化就乓。
通過 iTools -> 工具箱 -> 崩潰日志 -> 在以下路徑查看
# Mac ~/Library/Logs/CrashReporter/MobileDevice/<DEVICE_NAME> # Windows C://Users/<USER_NAME>/AppDataRoamingApple/ComputerLogsCrashReporterMobileDevice/<DEVICE_NAME>/
第三方 Crash 統(tǒng)計庫:KSCrash生蚁、plcrashReporter、CrashKit
-
第三方 Crash 統(tǒng)計服務(wù):Crashlytics戏自、Hockeyapp擅笔、友盟志衣、Bugly
注意:
- 最好只集成一個 Crash 統(tǒng)計服務(wù)屯援,當(dāng)各家的服務(wù)都以保證自己的Crash統(tǒng)計正確完整為目的時狞洋,難免出現(xiàn)時序手腳吉懊,強(qiáng)行覆蓋等等的惡意競爭假勿。
- 應(yīng)用層參與收集 Crash日志的服務(wù)方越多转培,越有可能影響iOS系統(tǒng)自帶的 Crash Reporter浸须。
二羽戒、Crash 符號化(Symbolicating crash logs)
symbols 和 Symbolicate
symbols 就是函數(shù)名或變量名易稠。符號化的過程就是把 crash log 中的內(nèi)存地址轉(zhuǎn)化為相應(yīng)的函數(shù)調(diào)用關(guān)系驶社。
一般來說企量,debug 模式構(gòu)建的 app 會把符號表存儲在編譯好的 binary 信息中届巩,而 release 模式構(gòu)建的app會把符號表存儲在 dSYM 文件中以節(jié)省體積恕汇。
系統(tǒng)庫符號化文件
每當(dāng) Xcode 連接一臺從未在當(dāng)前電腦調(diào)試過的 iOS 版本的設(shè)備時瘾英,都會花一段時間把手機(jī)的系統(tǒng)庫符號化文件自動導(dǎo)入到 ~/Library/Developer/Xcode/iOS DeviceSupport颂暇,這個過程叫 Processing symbol files耳鸯。每個系統(tǒng)版本的 symbols 文件約占 2GB,所以這個文件夾會占用不少磁盤空間添谊。但是碉钠,最好將這些內(nèi)容備份到外置硬盤喊废,需要符號化的時候再重新拷貝回來污筷,而不是使用清理工具清理掉瓣蛀。因為惋增,系統(tǒng)符號化文件的獲取沒有那么容易诈皿。
系統(tǒng)庫符號文件不是通用的稽亏,而是對應(yīng)crash所在設(shè)備的系統(tǒng)版本和CPU型號的截歉。獲取系統(tǒng)符號化文件的兩大方式就是通過真機(jī)瘪松,或者通過各版本 Xcode 附帶凉逛,蘋果官方?jīng)]有提供任何下載方式。有技術(shù)員總結(jié)了搜集方式书斜,并給出了 github 下載方式荐吉,可查看附錄样屠。
通過 Xcode 符號化
需要3個文件痪欲,放在同一目錄下
- crash報告(.crash文件)
- Debug Symbol 符號文件 (.dsym文件)
- 解壓 ipa 包后的 .app 文件
操作過程:Xcode -> Devices and Simulators -> 選中設(shè)備 -> View Device Logs
然后把 .crash文件 拖到 Device Logs 或者選擇下面的import導(dǎo)入.crash文件业踢。這樣你就可以看到crash的詳細(xì)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
# 引入環(huán)境變量
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 的時候很好用劳闹。用于符號化單個地址(可使用腳本批量化)本涕。
每一個可執(zhí)行程序都有一個build UUID來唯一標(biāo)識(每次 build 都不同)菩颖。Crash日志包含發(fā)生crash的這個應(yīng)用(app)的 build UUID以及crash發(fā)生的時候晦闰,應(yīng)用加載的所有庫文件的[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 進(jìn)行匹配
# 用 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
# 另外仰挣,應(yīng)用內(nèi) 獲取 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 網(wǎng)站來下載 dSYM 的話,對下載下來的每個 dSYM 文件都執(zhí)行一次
xcrun dsymutil -symbol-map ~/Library/Developer/Xcode/Archives/[...]/BCSymbolMaps [UUID].dSYM
示例:
# 有兩行未符號化的 crash log
* 3 appName 0x000f462a 0x4000 + 984618
* 4 appName **0x00352aee** 0x4000 + 3468014
# 1. 執(zhí)行
xcrun atos -o appName.app.dSYM/Contents/Resources/DWARF/appName -l 0x4000 -arch armv7
# 2. 然后輸入 0x00352aee
# 3. 符號化結(jié)果:
-[UIScrollView(UITouch) touchesEnded:withEvent:] (in appName) (UIScrollView+UITouch.h:26)
注意:
- 使用 symbolicatecrash颓芭,先拷貝出 symbolicatecrash 文件比較方便亡问。
- 無論是 symbolicatecrash 還是 atos州藕,都只需要
.crash
和.dSYM
床玻,或.crash
和.app
锈死,就可以符號化了馅精。 - 系統(tǒng)方法的堆棧符號化需要系統(tǒng)符號化文件,如果本地 macOS 沒有該文件压彭,也沒有該版本 iOS 設(shè)備可拷貝壮不,可通過 Github iOS-System-Symbols(iOS 各版本系統(tǒng)符號庫) 下載询一,
三健蕊、Crash 文件結(jié)構(gòu)
1. Process Information(進(jìn)程信息)
Incident Idnetifier | 崩潰報告的唯一標(biāo)識符缩功,不同的Crash |
---|---|
CrashReporter Key | 設(shè)備的 id(不是 uuid)嫡锌。通常同一個設(shè)備上同一版本的 app 發(fā)生Crash時,該值都是一樣的啦桌。 |
Hardware Model | 設(shè)備類型 |
Process | 進(jìn)程名稱[進(jìn)程 id]震蒋,進(jìn)程通常是 app 名字 |
Path | 可執(zhí)行程序的位置 |
Identifier | com.companyName.appName |
Version | app 版本號 |
Code Type | CPU 架構(gòu) |
Parent Process | 父進(jìn)程查剖,iOS中App通常都是單進(jìn)程的笋庄,一般父進(jìn)程都是 launchd |
2. Basic Information(基本信息)
Date/Time | Crash發(fā)生的時間菌仁,可讀的字符串 |
---|---|
OS Version | 系統(tǒng)版本(build 號) |
Report Version | Crash日志的格式济丘,目前基本上都是104摹迷,不同的version里面包含的字段可能有不同 |
3. Exception(異常)
Exception Type | 異常類型 |
---|---|
Exception Subtype: | 異常子類型 |
Crashed Thread | 發(fā)生異常的線程號 |
Exception Information | 額外診斷信息 |
從macOS Sierra, iOS 10, watchOS 3, 和 tvOS 10開始峡碉,額外診斷信息,包括:
應(yīng)用的具體信息:在進(jìn)程被終止前捕捉到的框架錯誤信息
內(nèi)核信息:關(guān)于代碼簽名問題的細(xì)節(jié)
Dyld (動態(tài)鏈接庫)錯誤信息:被動態(tài)鏈接器提交的錯誤信息
# 一段因為找不到鏈接庫而導(dǎo)致進(jìn)程被終止的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而導(dǎo)致進(jìn)程被終止的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 發(fā)生時的線程的調(diào)用棧地来,沒有符號化前是內(nèi)存地址。
5. Thread State(線程狀態(tài))
Crash 發(fā)生時的寄存器狀態(tài)颂碧。在你讀一個 Crash Report 的時候载城,了解線程狀態(tài)并非必須诉瓦,但是如果你想更好地了解crash的細(xì)節(jié)睬澡,這會起一些幫助,這需要一些處理器硬件只是和匯編知識的儲備昔脯。
LLDB與匯編調(diào)試-提高你的調(diào)試效率
6. Binary Images(二進(jìn)制映像)
Crash 發(fā)生時 app 可執(zhí)行文件云稚、加載的所有系統(tǒng)庫和第三方庫静陈。
# app 可執(zhí)行文件 Elephant
0x104e80000 - 0x107b2bfff +Elephant arm64 <38c058044caa34818a83d88981986fad> /var/containers/Bundle/Application/5694FC83-018E-46E7-B060-A008867D3C9D/Elephant.app/Elephant
# WCDB 可執(zhí)行文件贵白。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
四、Crash 的類型
4.1 兩類主要的 Crash
引發(fā)崩潰的代碼本質(zhì)上就兩類角撞,
一類是 c/c++ 語言層面的錯誤谒所,比如野指針,除零尖淘,內(nèi)存訪問異常等等(相對復(fù)雜)
對于前者村生,無論是 iOS 還是 Android 系統(tǒng)趁桃,其底層都是 unix 或者是類 unix 系統(tǒng),都可以通過信號機(jī)制來獲取 signal 或者是 sigaction (但是只能捕捉有限的幾種類型)卫病,設(shè)置一個回調(diào)函數(shù)。
- Watchdog 超時屹逛、用戶強(qiáng)制退出罕模、低內(nèi)存終止等蒿讥,系統(tǒng)拋出Unix信號芋绸,沒有任何的錯誤堆棧信息
另一類是未捕獲異常 Uncaught Exception(相對簡單)
iOS 下面最常見的就是 Objective-C 的NSException(@throw 拋出)摔敛,可以使用NSUncaughtExceptionHandler catch 住防止崩潰。
- 如數(shù)組越界行楞,給對象發(fā)送了無法識別的消息(selector方法沒有實(shí)現(xiàn)子房,對象調(diào)用方法出錯)等证杭,系統(tǒng)拋出一個NSException對象,對象中有出錯的堆棧琢歇,描述了出錯的代碼位置李茫、類名和方法名
4.1.1 Bus Error
- Non-existent address(訪問不存在的內(nèi)存地址)
- Unaligned access(訪問未對齊的內(nèi)存地址)
- Paging errors(分頁錯誤)
在檢測順序上,先檢測 SIGBUS宠互,再檢測 SIGSEGV予跌。
SIGBUS 地址被放到地址總線之后频轿,檢測出地址不對齊航邢,發(fā)出異常信號,
SIGSEGV 地址已經(jīng)放到地址總線上后秽之,在后續(xù)流程中檢測出內(nèi)存違法訪問,發(fā)出異常信號。
-
SIGBUS (Bus error)訪問非法地址
指針?biāo)鶎?yīng)的地址是有效地址震叙,但總線不能正常使用該指針媒楼,通常是未對齊的數(shù)據(jù)訪問所致划址。
一些處理器架構(gòu)上要求對齊訪問數(shù)據(jù),比如只能從4字節(jié)邊界上讀取一個4字節(jié)的數(shù)據(jù)類型(對于長度4個字節(jié)的對象世澜,其存放地址起碼要被4整除才可以)嵌洼。否則向當(dāng)前進(jìn)程分發(fā)SIGBUS信號麻养。
-
SIGSEGV (Segmentation fault回溺、segfault)合法地址的非法訪問
在 ARC 后很少遇到,意味著指針?biāo)鶎?yīng)的地址是無效地址混萝,沒有物理內(nèi)存對應(yīng)該地址遗遵。
訪問不屬于本進(jìn)程的內(nèi)存地址
往沒有寫權(quán)限的內(nèi)存地址寫數(shù)據(jù)
訪問已被釋放的內(nèi)存
-
SEGV(Segmentation Violation)
代表無效內(nèi)存地址,比如空指針逸嘀,未初始化指針车要,棧溢出等崭倘。
4.1.2 其他異常類型
-
EXC_CRASH(SIGABRT)
情形:Abnormal Exit 異常退出
未捕獲的 Objective-C 異常(NSException)翼岁,導(dǎo)致系統(tǒng)發(fā)送了 Abort 信號退出,導(dǎo)致這類異常崩潰的原因是捕獲到 Objective-C/C++ 異常司光,并且調(diào)用了 abort() 函數(shù),會在斷言/app內(nèi)部/操作系統(tǒng)用終止方法拋出琅坡。
- 通常發(fā)生在異步執(zhí)行系統(tǒng)方法的時候,如CoreData/NSUserDefaults等,還有一些其他的系統(tǒng)多線程操作残家。這并不一定意味著是系統(tǒng)代碼存在bug榆俺,代碼僅僅是成了無效狀態(tài),或者異常狀態(tài)。
- 通常Foundation庫中的容器為了保護(hù)狀態(tài)正常會做一些檢測坞淮,例如插入nil到數(shù)組中等會遇到此類錯誤茴晋。
- App Extensions,例如輸入法回窘,如果花了太多時間做初始化的話就會以這種異常退出(看門狗機(jī)制)诺擅。如果擴(kuò)展程序由于在啟動時掛起進(jìn)而被kill掉,那 Report 中的Exception Subtype字段會寫 LAUNCH_HANG啡直。因為擴(kuò)展App沒有main函數(shù)烁涌,所以任何情況下的在static constructors和+load方法里的初始化時間都會體現(xiàn)在你的擴(kuò)展或者依賴庫中。因此你應(yīng)當(dāng)盡可能的推遲這些邏輯付枫。
example 1: unrecognized selector sent to instance example 1: attempt to insert nil object from objects
對于可能在別處被釋放的對象烹玉,要自己持有一份(alloc 或 copy)。
-
EXC_BREAKPOINT(SIGTRAP)
情形:Trace Trap 追蹤捕獲
和進(jìn)程異常退出類似阐滩,這種異常是由于在特殊的節(jié)點(diǎn)加入debugger調(diào)試節(jié)點(diǎn)二打,如果當(dāng)前沒有調(diào)試器(debugger)依附,那么則會導(dǎo)致進(jìn)程被殺掉掂榔〖绦В可以通過 __builtin_trap() 在代碼里手動出發(fā)這種異常症杏。
這種 Crash 在 iOS 底層的框架中經(jīng)常出現(xiàn),最常見的是GCD瑞信。底層庫(例如libdispatch)會在遇到fatal錯誤的時候陷入這個困局厉颤。
-
Swift代碼會在運(yùn)行時的時候遇到下述問題時拋出這種異常:
一個non-optional的類型被賦予一個nil值
一個失敗的強(qiáng)制轉(zhuǎn)換
遇到這種錯誤,查下堆棧信息并想清楚是在哪里遇到了未知情況(unexpected condition)凡简。額外信息也可能會在設(shè)備的控制臺的日志里出現(xiàn)逼友。你應(yīng)當(dāng)盡量修改你的代碼,去優(yōu)雅的處理這種運(yùn)行時錯誤秤涩。例如帜乞,處理一個optional的值,通過可選綁定(Optional binding)而不是強(qiáng)制解包來獲得其值筐眷。
-
EXC_BAD_INSTRUCTION(SIGILL)
情形:Illegal Instruction 非法指令
當(dāng)嘗試去執(zhí)行一個非法或者未定義的指令時會觸發(fā)該異常黎烈。有可能是因為線程在一個配置錯誤的函數(shù)指針的誤導(dǎo)下嘗試jump到一個無效地址。
在Intel處理器上匀谣,ud2操作碼會導(dǎo)致一個EXC_BAD_INSTRUCTION異常照棋,但是這個通常用來做調(diào)試用途。在Intel處理器上武翎,Swift會在運(yùn)行時碰到未知情況時被停止烈炭。 詳情參考Trace Trap。
-
SIGKILL
情形:Killed
進(jìn)程收到系統(tǒng)指令被干掉宝恶。請自行查看Termination Reason(會包含一個命名空間和代碼)來定位線程被干掉的原因梳庆。
-
SIGQUIT
情形:Quit 退出
這個異常是由于其它進(jìn)程擁有高優(yōu)先級且可以管理本進(jìn)程(因此被高優(yōu)先級進(jìn)程Kill掉)所導(dǎo)致。SIGQUIT不代表進(jìn)程發(fā)生Crash了卑惜,但是它確實(shí)反映了某種不合理的行為。
iOS中驻售,如果占用了太長時間露久,鍵盤擴(kuò)展程序會隨著宿主app被干掉。因此欺栗,這種情況的異常下不太可能會在Crash Report中出現(xiàn)合理可讀的異常代碼毫痕。大概率是因為一些其它代碼在啟動時占用了太長時間但是在總時間限制前(看門狗的時間限制,見上文中的表格)成功結(jié)束了迟几,但是執(zhí)行邏輯在extension退出的時候被錯誤的執(zhí)行了消请。你應(yīng)該運(yùn)行Profile,仔細(xì)分析一下extension的各部分消耗時間类腮,把耗時較多的邏輯放到background或者推遲(推遲到extension加載完畢)臊泰。
-
EXC_ARITHMETIC
除零錯誤會拋出此類異常
arithmetic [?'r?θm?t?k] 算術(shù),算法
-
SIGPIPE 管道破裂
這個信號通常在進(jìn)程間通信產(chǎn)生蚜枢,比如采用FIFO(管道)通信的兩個進(jìn)程缸逃,讀管道沒打開或者意外終止就往管道寫针饥,寫進(jìn)程會收到SIGPIPE信號。
此外用Socket通信的兩個進(jìn)程需频,寫進(jìn)程在寫Socket的時候丁眼,讀進(jìn)程已經(jīng)終止。
對一個端已經(jīng)關(guān)閉的socket調(diào)用兩次寫入操作昭殉,第二次寫入將會產(chǎn)生SIGPIPE信號苞七,該信號默認(rèn)結(jié)束進(jìn)程。
// 預(yù)防方式挪丢,寫在 pch 文件 // 僅在 iOS 系統(tǒng)上支持 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
另外一些異常類型
為了防止一個應(yīng)用占用過多的系統(tǒng)資源蹂风,設(shè)計了 watchdog 的機(jī)制, watchdog 會監(jiān)測應(yīng)用的性能。如果超出了該場景所規(guī)定的運(yùn)行時間吃靠,watchdog 強(qiáng)制終結(jié)這個應(yīng)用的進(jìn)程硫眨。
Exception Code 說明 0xbaaaaaad 并非一個真正的Crash,由用戶同時按Home鍵和音量鍵觸發(fā)巢块。 0xbad22222 當(dāng)VoIP程序在后臺太過頻繁的激活時礁阁,系統(tǒng)可能會終止此類程序。 0x8badf00d(badfood) launch/resume/suspend/quit/background 響應(yīng)超過規(guī)定時間會被 Watchdog 終止(詳見下表)族奢, 并產(chǎn)生一個崩潰日志姥闭。在連接Xcode調(diào)試時為了便于調(diào)試,系統(tǒng)會暫時禁用掉Watchdog越走,所以此類問題的發(fā)現(xiàn)需要使用正常的啟動模式棚品。 0xc00010ff 程序執(zhí)行大量耗費(fèi)CPU和GPU的運(yùn)算,導(dǎo)致設(shè)備過熱廊敌,觸發(fā)系統(tǒng)過熱保護(hù)被系統(tǒng)終止铜跑。這個也許是和發(fā)生crash的特定設(shè)備有關(guān),或者是和它所在的環(huán)境有關(guān)骡澈。 0xdead10cc(deadlock) 程序退到后臺時還占用系統(tǒng)資源(如通訊錄)被系統(tǒng)終止锅纺。或者程序掛起時拿到了文件鎖或者sqlite數(shù)據(jù)庫所長期不釋放直到被凍結(jié)肋殴。如果你的app在掛起時拿到了文件鎖或者sqlite數(shù)據(jù)庫鎖囤锉,它必須請求額外的后臺執(zhí)行時間(request additional background execution time )并在被掛起前完成解鎖操作。 0xdeadfa11(deadfall) 程序無響應(yīng)用戶強(qiáng)制退出护锤。當(dāng)用戶長按電源鍵官地,直到屏幕出現(xiàn)關(guān)機(jī)確認(rèn)畫面后再長按Home鍵,將強(qiáng)制退出應(yīng)用烙懦。(不是雙擊 home 的強(qiáng)退)Exception Note 會有 SIMULATED 字段 0x2bad45ec app因為安全違規(guī)操作被iOS系統(tǒng)終止驱入。終止描述會寫:“進(jìn)程被查到在安全模式進(jìn)行非安全操作”,暗示app嘗試在禁止屏幕繪制的時候繪制屏幕,例如當(dāng)屏幕鎖定時沧侥。用戶可能會忽略這種異常可霎,尤其當(dāng)屏幕是關(guān)閉的或者當(dāng)這種終止發(fā)生時正好鎖屏。 說明:通過App Switcher(就是雙擊home鍵出現(xiàn)的那個界面)并不會生成Crash Report宴杀。一旦app進(jìn)入掛起狀態(tài)癣朗,被iOS在任何時間終止掉都是合理的,因此這時候不會生成Crash Report旺罢。
以下異常代碼只針對 watchOS
Exception Code 說明 0xc51bad01 在后臺任務(wù)占用了過多的cpu時間而導(dǎo)致watch app被干掉旷余。想要解決這個問題,優(yōu)化后臺任務(wù)扁达,提高CPU執(zhí)行效率正卧,或者減少后臺的任務(wù)運(yùn)行數(shù)量。 0xc51bad02 在后臺的規(guī)定時間內(nèi)沒有完成指定的后臺任務(wù)而導(dǎo)致watch app被干掉跪解。想要解決這個問題炉旷,需要當(dāng)app在后臺運(yùn)行時減少app的處理任務(wù)优烧。 0xc51bad03 沒有在規(guī)定時間內(nèi)完成后臺任務(wù)套腹,且系統(tǒng)一直非常忙以至于app無法獲取足夠的CPU時間來完成后臺任務(wù)。雖然一個app可以通過減少自身在后臺的運(yùn)行任務(wù)來避免這個問題端考,但是0xc51bad03這個錯誤把矛頭指向了過高的系統(tǒng)負(fù)載图仓,而非app本身有什么問題罐盔。 -
附:Watchdog 超時時間
場景 超時時間 launch(啟動) 20s resume(恢復(fù)) 10s suspend(掛起) 10s quit(退出) 6s background(后臺) 10min 簡單說,就是以下代理必須在規(guī)定時間內(nèi)執(zhí)行完畢救崔,讓程序響應(yīng)起來惶看。
- (void)applicationDidFinishLaunching:(UIApplication *)application; - (void)applicationDidBecomeActive:(UIApplication *)application; - (void)applicationWillResignActive:(UIApplication *)application; - (void)applicationDidEnterBackground:(UIApplication *)application; - (void)applicationWillEnterForeground:(UIApplication *)application; - (void)applicationWillTerminate:(UIApplication *)application;
崩潰(準(zhǔn)確的說是程序異常終止)是程序接收到未處理信號的結(jié)果。
未處理信號有三個來源:內(nèi)核六孵、其他進(jìn)程和應(yīng)用本身纬黎。導(dǎo)致崩潰最常見的兩個信號如下:
- EXC_BAD_ACCESS 是一種由內(nèi)核發(fā)出的Mach異常,通常是因為應(yīng)用試圖訪問不存在的內(nèi)存空間導(dǎo)致的劫窒。如果未能在Mach內(nèi)核級別進(jìn)行處理莹桅,它將被轉(zhuǎn)化為SIGBUS或者SIGSEGV BSD信號。
- SIGABRT是當(dāng)產(chǎn)生未捕獲的NSException或者obj_exception_throw時烛亦,應(yīng)用發(fā)給自身的BSD信號。
在 Objective-C 異常中懂拾,導(dǎo)致異常拋出最常見的原因是應(yīng)用向?qū)ο蟀l(fā)送了未實(shí)現(xiàn)的方法選擇器(比如拼寫錯誤煤禽,對象混淆或者向已經(jīng)釋放的對象發(fā)送消息)。
4.2 Low Memory Report 低內(nèi)存報告
Low Memory Termination
跟一般的Crash結(jié)構(gòu)不太一樣岖赋,通常有Free pages檬果,Wired Pages,Purgeable pages,largest process 組成选脊,同時會列出當(dāng)前時刻系統(tǒng)運(yùn)行所有進(jìn)程的信息杭抠。
Low Memory Report 與其它 Crash Report 不同,它沒有堆棧信息恳啥,所以不需要符號化偏灿。一個低內(nèi)存 Report的Header會和 Crash Report 的header有些類似。緊接著Header的時各個字段的系統(tǒng)級別的內(nèi)存統(tǒng)計信息钝的。記錄下頁大形檀埂(Page Size)字段。每一個進(jìn)程的內(nèi)存占用大小是根據(jù)內(nèi)存的頁的數(shù)量來 Report的硝桩。一個低內(nèi)存 Report最重要的部分是進(jìn)程表格沿猜。這個表格列出了所有的運(yùn)行進(jìn)程,包括系統(tǒng)在生成低內(nèi)存 Report時的守護(hù)進(jìn)程碗脊。如果一個進(jìn)程被”遺棄”了啼肩,會在[原因]一列附上具體的原因。一個進(jìn)程可能被遺棄的原因有:
-
[per-process-limit]
進(jìn)程占用超過了它的最大內(nèi)存值衙伶。每一個進(jìn)程在常駐內(nèi)存上的限制是早已經(jīng)由系統(tǒng)為每個應(yīng)用分配好了的祈坠。超過這個限制會導(dǎo)致進(jìn)程被系統(tǒng)干掉。
注意:擴(kuò)展程序(nimo: Extension app, 例如輸入法等)的最大內(nèi)存值更少痕支。一些技術(shù)颁虐,例如地圖視圖和SpriteKit,占用非常多的基礎(chǔ)內(nèi)存卧须,因此不適合用在擴(kuò)展程序里另绩。
-
[vm-pageshortage]/[vm-thrashing]/[vm]
由于系統(tǒng)內(nèi)存壓力被干掉。
-
[vnode-limit]
打開太多文件了花嘶。
注意:系統(tǒng)會盡量避免在vnodes已經(jīng)枯竭的時候干掉高頻app笋籽。因此你的應(yīng)用如果在后臺,即便并沒有占用什么vnode椭员,而有可能被殺掉车海。
-
[highwater]
一個系統(tǒng)守護(hù)進(jìn)程超過過了它的內(nèi)存占用高水位(就是已經(jīng)很危險了)。
-
[jettisoned]
進(jìn)程因為其它不可描述的原因被殺掉隘击。
當(dāng)你發(fā)現(xiàn)一個低內(nèi)存crash侍芝,與其去擔(dān)心哪一部分的代碼出現(xiàn)問題,還不如仔細(xì)審視一下自己的內(nèi)存使用習(xí)慣和針對低內(nèi)存告警(low-memory warning)的處理措施埋同。Locating Memory Issues in Your App 列出了如何使用Leaks Instrument工具來檢查內(nèi)存泄漏州叠,和如何使用Allocations Instrument的Mark Heap 功能來避免內(nèi)存浪費(fèi)。 Memory Usage Performance Guidelines 討論了如何處理接受到低內(nèi)存告警的問題凶赁,以及如何高效使用內(nèi)存咧栗。當(dāng)然逆甜,也推薦你去看下2010年的WWDC中的 Advanced Memory Analysis with Instruments 那一章節(jié)。
重要:Leaks和Allocation工具不能檢測所有的內(nèi)存使用情況致板。你需要和VM Tracker工具一起運(yùn)行(包含在Allocation工具里)來查看你的內(nèi)存運(yùn)行交煞。默認(rèn)VM Tracker是不可用的。如果想通過VM Tracker來profile你的應(yīng)用斟或,點(diǎn)擊instrument工具素征,選中”Automatic Snapshotting”標(biāo)簽或者手動點(diǎn)擊”Snapshot Now”按鈕。
五缕粹、Crash 的捕獲
5.0 Last Exception Backtrace
若程序因 NSException 而 Crash稚茅,系統(tǒng)日志中的 Last Exception Backtrace 信息是完整準(zhǔn)確的,不會受應(yīng)用層的 Crash 統(tǒng)計服務(wù)影響平斩,可作為排查問題的參考線索亚享。如果 Last Exception Backtrace,只包含16進(jìn)制信息的日志绘面,必須進(jìn)行符號化來獲取有價值的堆棧信息
# 未符號化的異常堆棧
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)
有兩種方式可以捕獲那些會導(dǎo)致崩潰的未捕獲狀態(tài)欺税。
- 使用 NSUncaughtExceptionHandler 函數(shù)來安裝未捕獲 Objective-C 異常的處理器。
- 使用 signal 函數(shù)來安裝 BSD 信號處理器揭璃。
注意:signal 要在沒有附加 debugger 的環(huán)境下獲取晚凿,否則會被 debugger 優(yōu)先攔截。UncaughtExceptionHandler可以在調(diào)試狀態(tài)下捕獲
抓取 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);
}
// 對于異常和信號的響應(yīng)會在 MyUncaughtExceptionHandler 和 SignalHandler 中實(shí)現(xiàn)瘦馍。在樣例程序中歼秽,以上二者的處理方式相同。
void MyUncaughtExceptionHandler(NSException *exception) {
NSString *ret = [NSString stringWithFormat:@"異常名稱:\n%@\n\n異常原因:\n%@\n\n出錯堆棧內(nèi)容:\n%@\n",exception.name, exception.reason, exception.callStackSymbols];
// 將捕獲到的 exception 細(xì)節(jié)上傳到后臺
}
抓取 Signal
signal信號是Unix系統(tǒng)中的,是一種異步通知機(jī)制.信號傳遞給進(jìn)程后,在沒有處理函數(shù)的情況下,程序可以指定三種行為:
- 忽略該信號,但是對于信號
SIGKILL
和SIGSTOP
不可忽略 - 使用默認(rèn)的處理函數(shù)
SIG_DFL(即 signal(sig, SIG_DFL);)
情组,大多數(shù)信號的默認(rèn)動作是終止進(jìn)程 - 捕獲信號,執(zhí)行用戶定義的函數(shù)
有兩個特殊的常量:
-
SIG_IGN
燥筷,向內(nèi)核表示忽略此信號.對于不能忽略的兩個信號SIGKILL
和SIGSTOP
,調(diào)用時會報錯 -
SIG_DFL
院崇,執(zhí)行該信號的系統(tǒng)默認(rèn)動作.
還有兩個常用的函數(shù)
-
int kill(pid_t pid, int signo);
,發(fā)送信號到指定的進(jìn)程 -
int raise(int signo);
,發(fā)送信號給自己.
// UNIX系統(tǒng)中常用的信號有以下幾種:
SIGABRT--程序中止命令中止信號
SIGBUS--程序內(nèi)存字節(jié)未對齊中止信號
SIGFPE--程序浮點(diǎn)異常信號
SIGILL--程序非法指令信號
SIGSEGV--程序無效內(nèi)存中止信號
SIGTERM--程序kill中止信號
SIGKILL--程序結(jié)束接收中止信號
SIGALRM--程序超時信號
SIGHUP--程序終端中止信號
SIGINT--程序鍵盤中斷信號
SIGSTOP--程序鍵盤中止信號
SIGPIPE--程序Socket發(fā)送失敗中止信號
// 抓取的是以下幾種
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);
}
// 抓取信號的處理函數(shù)
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èi)容:\n",name];
for (i =0; i <frames; i++) {
[exceptionStr appendFormat:@"%s\n",strs[i]];
}
free(strs);
}
// 在應(yīng)用崩潰后肆氓,保持運(yùn)行狀態(tài)而不退出,讓響應(yīng)更加友好
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拨扶。它們會終止或者暫停應(yīng)用。(SIGKILL是命令行函數(shù)kill -9發(fā)出的茁肠,SIGSTOP是鍵入Control-Z發(fā)出的)屈雄。
如果你發(fā)現(xiàn)本應(yīng)該被捕捉的異常并沒有被捕捉到,請確定您沒有在building應(yīng)用或者library時添加了-no_compact_unwind標(biāo)簽官套。
64位 iOS 用了zero-cost的異常實(shí)現(xiàn)機(jī)制。在zero-cost系統(tǒng)里,每一個函數(shù)都有一個額外的數(shù)據(jù)奶赔,它會描述如果一個異常在跨函數(shù)范圍內(nèi)實(shí)現(xiàn)惋嚎,該如何展開相應(yīng)的堆棧信息。如果一個異常發(fā)生在多個堆棧但是沒有可展開的數(shù)據(jù)站刑,那么異常處理函數(shù)自然無法跟蹤并記錄另伍。也許在堆棧很上層的地方有異常處理函數(shù),但是如果那里沒有一個片段的可展開信息绞旅,沒辦法從發(fā)生異常的地方到那里摆尝。指定了-no_compact_unwind標(biāo)簽表明你那些代碼沒有可展開信息,所以你不能跨越函數(shù)拋出異常(也就是說無法通過別的函數(shù)捕捉當(dāng)前函數(shù)的異常)因悲。
5.2 Xcode 提供的調(diào)試工具
都在 Edit Scheme -> Diagnostics(診斷) 依次可以找到
Runtime Sanitization
-
Address Sanitizer(地址消毒劑)
AddressSanitizer的原理是當(dāng)程序創(chuàng)建變量分配一段內(nèi)存時堕汞,將此內(nèi)存后面的一段內(nèi)存也凍結(jié)住,標(biāo)識為中毒內(nèi)存晃琳。當(dāng)程序訪問到中毒內(nèi)存時(越界訪問)讯检,就會拋出異常,并打印出相應(yīng)log信息卫旱。調(diào)試者可以根據(jù)中斷位置和的log信息人灼,識別bug。如果變量釋放了顾翼,變量所占的內(nèi)存也會標(biāo)識為中毒內(nèi)存投放,這時候訪問這段內(nèi)存同樣會拋出異常(訪問已經(jīng)釋放的對象)。
-
Thread Sanitizer
用于解決多線程問題:如何用Xcode8解決多線程問題
- Use of uninitialized mutexes(使用未初始化的互斥器)
- Thread leaks (missing pthread_join) 線程泄漏(缺少pthread_join)
- Unsafe calls in signal handlers (ex:malloc) 信號處理程序中的不安全調(diào)用(例如:malloc)
- Unlock from wrong thread 從錯誤的線程解鎖
- Data races 數(shù)據(jù)競爭(只要涉及到多線程編程适贸,遇到的概率非常之高灸芳,寫多線程代碼時最容易遇到的問題,一旦踩坑取逾,現(xiàn)象往往是偶現(xiàn)的耗绿,難以調(diào)試)
大致原理是記錄每個線程訪問變量的信息來做分析,值得一提的是砾隅,現(xiàn)階段的Thread Sanitizer最多只同時記錄4個線程的訪問信息误阻,在復(fù)雜的場景下,可能出現(xiàn)偶爾檢測不出data race的場景晴埂,所以需要長時間經(jīng)常性的運(yùn)行來盡可能多的發(fā)現(xiàn)data race究反,這也是為什么蘋果建議默認(rèn)開啟Thread Sanitizer,而且Thread Sanitizer 造成的額外性能損耗非常之小儒洛。
Thread Sanitizer 現(xiàn)階段只能在模擬器環(huán)境下執(zhí)行精耐,真機(jī)還不支持,有同學(xué)測試發(fā)現(xiàn)琅锻,只支持64位系統(tǒng)卦停,也就是說iPhone 5及其更早的模擬器也不支持向胡,iPhone 5s 之后才是64位系統(tǒng)。
Memory Management
-
Malloc Scribble
申請內(nèi)存后在申請的內(nèi)存上填 0xAA惊完,內(nèi)存釋放后在釋放的內(nèi)存上填 0x55僵芹;再就是說如果內(nèi)存未被初始化就被訪問,或者釋放后被訪問小槐,就會引發(fā)異常拇派,這樣就可以使問題盡快暴漏出來。
Scribble 其實(shí)是 malloc 庫 libsystem_malloc.dylib 自身提供的調(diào)試方案
-
Malloc Guard Edges
申請大片內(nèi)存的時候在前后page上加保護(hù)凿跳,詳見保護(hù)模式件豌。
-
Guard Malloc
使用 libgmalloc 捕獲常見的內(nèi)存問題,比如越界控嗜、釋放之后繼續(xù)使用茧彤。
由于 libgmalloc 在真機(jī)上不存在,因此這個功能只能在模擬器上使用.
-
Zombie Objects(僵尸對象)
Instrument 也有一個 Zombie 工具躬审,使用起來差不多棘街。
Zombie 的原理是用生成僵尸對象來替換 dealloc 的實(shí)現(xiàn),當(dāng)對象引用計數(shù)為 0 的時候承边,將需要 dealloc 的對象轉(zhuǎn)化為僵尸對象遭殉。如果之后再給這個僵尸對象發(fā)消息,則拋出異常博助,并打印出相應(yīng)的信息险污,調(diào)試者可以很輕松的找到異常發(fā)生位置。
如果 objc_msgSend 或者 objc_release出現(xiàn)在crash的線程的附近富岳,則進(jìn)程有可能嘗試去給一個被釋放的對象發(fā)送消息蛔糯,那么可使用 Zombie 調(diào)試
# 控制臺會多一些調(diào)試信息 message sent to deallocated instance 0x60800000c380
Analyze(靜態(tài)代碼分析)
不是那么準(zhǔn)確,但是會發(fā)現(xiàn)一些問題
可以發(fā)現(xiàn)編譯中的 warning窖式,內(nèi)存泄漏隱患蚁飒,甚至還可以檢查出邏輯上的問題;所以在自測階段一定要解決Analyze發(fā)現(xiàn)的問題萝喘,可以避免出現(xiàn)嚴(yán)重的bug淮逻。
主要分析以下四種問題:
- 邏輯錯誤:訪問空指針或未初始化的變量等;
- 內(nèi)存管理錯誤:如內(nèi)存泄漏等阁簸;
- 聲明錯誤:從未使用過的變量爬早;
- 調(diào)用錯誤:未包含使用的庫和框架。
# 內(nèi)存泄漏隱患
Potential(潛在) Leak of an object allocated on line ……
# 數(shù)據(jù)賦值隱患
The left operand of …… is a garbage value;
# 對象引用隱患
Reference-Counted object is used after it is released;
Profile(就是運(yùn)行 Instrument)
真正運(yùn)行程序启妹,對程序進(jìn)行內(nèi)存分析(查看內(nèi)存分配情況筛严、內(nèi)存泄露)
優(yōu)點(diǎn):分析非常準(zhǔn)確,如果發(fā)現(xiàn)有提示內(nèi)存泄露饶米,基本可以斷定代碼問題
缺點(diǎn):分析效率低(真正運(yùn)行了一段代碼桨啃,才能對該代碼進(jìn)行內(nèi)存分析)
六车胡、附錄
- Understanding and Analyzing iOS Application Crash Reports
- 了解和分析iOS Crash
- iOS異常捕獲
- Technical Note TN2123 CrashReporter
- Application does not crash when launched from debugger but crashes when launched by user
- iOS Crash分析必備:符號化系統(tǒng)庫方法
- Xcode中和symbols有關(guān)的幾個設(shè)置
- 漫談iOS Crash收集框架
- iOS崩潰信息收集
- 低于0.01%的極致Crash率是怎么做到的?
- iOS微信內(nèi)存監(jiān)控
- Xcode 利用工具解決程序內(nèi)存問題
- 如何定位Obj-C野指針隨機(jī)Crash(一):先提高野指針Crash率
- 如何定位Obj-C野指針隨機(jī)Crash(二):讓非必現(xiàn)Crash變成必現(xiàn)
- 如何定位Obj-C野指針隨機(jī)Crash(三):加點(diǎn)黑科技讓Crash自報家門