目錄
一奠支、崩潰收集介紹
二、第三方庫(kù)收集崩潰信息
三贵白、原生收集崩潰信息
四率拒、崩潰信息符號(hào)化
五、崩潰中斷攔截
一禁荒、崩潰收集介紹
??App線上崩潰一直都是比較棘手的問(wèn)題猬膨,盡管我們努力在開(kāi)發(fā)與測(cè)試過(guò)程中檢測(cè)與避免崩潰代碼,但依然會(huì)在不同系統(tǒng)呛伴,不同網(wǎng)絡(luò)環(huán)境與不同的使用方式中出現(xiàn)特殊情況勃痴。
??面對(duì)崩潰情況的最好解決方式就是對(duì)崩潰的程序進(jìn)行現(xiàn)場(chǎng)調(diào)試,顯然這很難做到热康。所以我們只能收集App崩潰時(shí)產(chǎn)生的信息沛申,通過(guò)對(duì)崩潰信息的解析來(lái)找到產(chǎn)生崩潰的原因,之后修復(fù)崩潰的代碼并發(fā)布新的版本姐军。
??市面上對(duì)于崩潰信息收集有很多種方案铁材,比如第三方的Bugly
,友盟
奕锌,KSCrash
衫贬,plcrashreporter
等,蘋果自帶的崩潰收集有crash文件
歇攻,crashes
固惯。
??本次會(huì)簡(jiǎn)單介紹并對(duì)比不同方案的優(yōu)劣以及其他關(guān)于崩潰收集的知識(shí)。
二缴守、第三方庫(kù)收集崩潰信息
??第三方庫(kù)主要介紹兩種類型葬毫,第一種是騰訊的Bugly
和友盟的APM
。這兩種都是將崩潰信息收集到自己的平臺(tái)進(jìn)行統(tǒng)計(jì)與符號(hào)化
①屡穗。第二種是KSCrash
贴捡,這是一個(gè)開(kāi)源的第三方庫(kù),它可以通過(guò)郵件或上傳的方式將收集到的崩潰信息發(fā)送到內(nèi)部服務(wù)器村砂,從而保證信息的安全性烂斋。
??首先是Bugly
與友盟
的對(duì)比:
??8月1日更新,友盟免費(fèi)版現(xiàn)只能查看當(dāng)天的崩潰記錄,其他時(shí)間的崩潰記錄需要開(kāi)會(huì)員才可以汛骂。
類別 | Bugly | 友盟(免費(fèi)版) | 區(qū)別 |
---|---|---|---|
最近更新時(shí)間 | 符號(hào)表工具更新時(shí)間2021-6 sdk更新時(shí)間為2021-4 |
sdk更新時(shí)間為2022-6 | |
集成方式 | pod / sdk | pod / sdk |
友盟需要橋接并且導(dǎo)入五個(gè)文件 |
啟動(dòng)方式 | Bugly.start() | UMConfigure.initWithAppkey() | |
上傳dSYM方式 | 通過(guò)命令上傳 | 在友盟網(wǎng)頁(yè)上傳 | Bugly需要jdk1.8環(huán)境才可以上傳 |
崩潰刷新時(shí)間 | 2分鐘左右 | 1-10分鐘甚至更久 | |
崩潰解析內(nèi)容 | 1.顯示崩潰原因 2.顯示待還原的堆棧名稱 3.顯示頁(yè)面操作流程 |
1.顯示還原后的堆棧名稱 2.顯示更詳細(xì)的手機(jī)信息 |
友盟崩潰原因只能手動(dòng)重現(xiàn) |
錯(cuò)誤分類 | 相同的崩潰不會(huì)出現(xiàn)多條數(shù)據(jù)罕模,只會(huì)增加次數(shù) | 相同的崩潰會(huì)出現(xiàn)多條數(shù)據(jù) |
??通過(guò)兩者的對(duì)比發(fā)現(xiàn),Bugly
更擅長(zhǎng)定位錯(cuò)誤原因帘瞭,友盟
更擅長(zhǎng)定位錯(cuò)誤代碼位置淑掌。
??我個(gè)人更推薦Bugly
,因?yàn)閷?duì)于崩潰信息Bugly
可以很清晰的展示出錯(cuò)誤原因蝶念,例如數(shù)組越界抛腕,空指針等。而友盟
只能展示崩潰的方法名媒殉,如果代碼過(guò)于復(fù)雜的話還需要多次調(diào)試才可以找到具體的崩潰原因担敌。
??Bugly
中想要還原堆棧名稱可以在終端使用命令:
xcrun swift-demangle [待還原的堆棧名稱]
# 例如:
# 未還原的堆棧名稱:
0x0000000100a27a04
$s9ErrorDemo14ViewControllerC05tableC0_14didSelectRowAtySo07UITableC0C_10Foundation9IndexPathVtFTf4dnn_n
# 還原命令(需要去掉開(kāi)頭的$字符)
xcrun swift-demangle s9ErrorDemo14ViewControllerC05tableC0_14didSelectRowAtySo07UITableC0C_10Foundation9IndexPathVtFTf4dnn_n
# 還原后的堆棧名稱:
ErrorDemo.ViewController.tableView(_: __C.UITableView, didSelectRowAt: Foundation.IndexPath)
# 通過(guò)還原后的堆棧名稱可以看出項(xiàng)目名稱,文件名稱以及崩潰的方法名稱廷蓉。
??不同于友盟
直接上傳dSYM
②的方式柄错,Bugly
上傳dSYM文件需要使用以下命令:
# 0.首先安裝 jdk1.8 環(huán)境(超過(guò)1.8的環(huán)境會(huì)出現(xiàn)無(wú)法打開(kāi)jar文件的問(wèn)題)
# 1.然后進(jìn)入到從 Bugly 官網(wǎng)下載的包含 buglyqq-upload-symbol.jar 文件的文件夾內(nèi)
# 2.在終端執(zhí)行上傳命令
java -jar buglyqq-upload-symbol.jar -appid [appid] -appkey [appkey] -bundleid [bundleid] -version [versionv] -platform IOS -inputSymbol [dSYM文件路徑]
# 例如
java -jar buglyqq-upload-symbol.jar -appid c441111b37 -appkey 36211ca0-1111-1111-1111-9c8b1111535a -bundleid com.fdm.ErrorDemo -version 1.0.2 -platform IOS -inputSymbol /Users/kenan/Desktop/ErrorDemo.app.dSYM
# 3.當(dāng)上傳狀態(tài)為200即代表上傳成功
??接下來(lái)是KSCrash
與上面兩種的對(duì)比:
??首先是它們的記錄方式不同
:Bugly
與友盟
可以通過(guò)手機(jī)信息,崩潰信息苦酱,崩潰類型等進(jìn)行一個(gè)統(tǒng)計(jì)與歸類售貌,方便開(kāi)發(fā)人員進(jìn)行查閱。而KSCrash
由于沒(méi)有自己的服務(wù)器疫萤,若需要統(tǒng)計(jì)歸類的話都需要開(kāi)發(fā)人員自行完成颂跨。
??第二是它們的符號(hào)化方式不同
:Bugly
與友盟
必須要開(kāi)發(fā)人員上傳dSYM文件才可以進(jìn)行正常符號(hào)化。而KSCrash
支持在設(shè)備上符號(hào)化扯饶,也就是說(shuō)App在收集到崩潰信息后可以在手機(jī)上進(jìn)行符號(hào)化恒削,然后將符號(hào)化后的崩潰信息存儲(chǔ)在手機(jī)中,上傳時(shí)直接上傳符號(hào)化后的崩潰信息尾序。
??第三是它們的安全性不同
:Bugly
與友盟
只能將崩潰信息上傳到它們的服務(wù)器中钓丰,而KSCrash
可以通過(guò)郵件或上傳的方式發(fā)送到本地的郵箱或服務(wù)器中,一定程度上保障了信息的安全性每币。
??最后是它們的代碼權(quán)限不同
:Bugly
與友盟
都是閉源的携丁,只能使用其提供的方法。而KSCrash
由于是開(kāi)源項(xiàng)目兰怠,可以對(duì)其功能進(jìn)行修改與擴(kuò)展梦鉴,也可以通過(guò)閱讀源碼的方式了解其底層的邏輯。
??對(duì)于崩潰日志的內(nèi)容揭保,KSCrash
所收集的崩潰日志與系統(tǒng)自帶分析中的崩潰日志幾乎相同肥橙,并且本地默認(rèn)以JSON字符串的方式對(duì)日志內(nèi)容進(jìn)行存儲(chǔ)。
??總結(jié):
??本次只對(duì)常規(guī)的屬性進(jìn)行了簡(jiǎn)單的崩潰測(cè)試秸侣,對(duì)于更復(fù)雜更詳細(xì)的功能與檢測(cè)方式還需要根據(jù)實(shí)際情況自行查看文檔與測(cè)試存筏。
??個(gè)人認(rèn)為對(duì)于常規(guī)的應(yīng)用使用Bugly
或友盟
足夠宠互,需要更豐富的功能或注重安全性的應(yīng)用可以考慮KSCrash
等類似的第三方庫(kù)。如果覺(jué)得這些還不夠用的話也可以考慮使用其他功能更豐富的收費(fèi)平臺(tái)椭坚。
三予跌、原生收集崩潰信息
??原生收集崩潰信息的方式我總結(jié)了三種,分別為:系統(tǒng)crash文件
藕溅,crashes工具
,代碼監(jiān)聽(tīng)
继榆。
??1.使用系統(tǒng)crash文件
收集:
??這種收集方式主要面向B端產(chǎn)品巾表。在用戶發(fā)生程序崩潰后,產(chǎn)品支持的同學(xué)可以通過(guò)設(shè)置
-> 隱私
-> 分析與改進(jìn)
略吨,在分析數(shù)據(jù)
的列表中找到對(duì)應(yīng)的APP名稱并打開(kāi)集币,然后點(diǎn)擊右上角的分享按鈕將錯(cuò)誤報(bào)告通過(guò)微信,郵件或其他方式發(fā)送給開(kāi)發(fā)人員翠忠。開(kāi)發(fā)人員拿到該文件后便可以通過(guò)符號(hào)化的方式找到發(fā)生崩潰的具體位置并修復(fù)該問(wèn)題鞠苟。
??這種方式除了收集不方便外還有一個(gè)問(wèn)題,就是系統(tǒng)只會(huì)保存第一次出現(xiàn)的崩潰日志秽之,后續(xù)如果因?yàn)橄嗤脑虮罎⒌庇椋到y(tǒng)不會(huì)創(chuàng)建新的日志,會(huì)出現(xiàn)在尋找崩潰日志時(shí)無(wú)法找到對(duì)應(yīng)日期的問(wèn)題考榨。
??注意:崩潰文件有可能是.ips后綴跨细,也有可能是.crash后綴,實(shí)際上通過(guò)直接修改后綴名的方式可以進(jìn)行轉(zhuǎn)換河质。如果崩潰內(nèi)容是JSON格式可以將后綴名改為.ips冀惭,看起來(lái)會(huì)更直觀。
??2.使用crashes工具
收集:
??crashes工具
是Xcode自帶的崩潰分析工具掀鹅。使用該工具收集崩潰信息需要用戶手機(jī)開(kāi)啟共享iphone分析
功能散休,具體操作流程為設(shè)置
-> 隱私
-> 分析與改進(jìn)
,開(kāi)啟共享iphone分析
乐尊,并開(kāi)啟與App開(kāi)發(fā)者共享
選項(xiàng)戚丸。之后應(yīng)用發(fā)生崩潰后,崩潰信息會(huì)自動(dòng)發(fā)送到Apple的服務(wù)器上扔嵌,當(dāng)你進(jìn)入crashes工具
時(shí)會(huì)自動(dòng)下載崩潰信息昏滴。缺點(diǎn)是崩潰信息上傳不及時(shí),會(huì)有幾天的延遲对人。
??你可以通過(guò)Xcode中的Window
-> Organizer
進(jìn)入打包頁(yè)面谣殊,在左上角選擇需要查看的App,點(diǎn)擊左側(cè)crashes
選項(xiàng)即可顯示崩潰列表牺弄。
??崩潰列表上方為分類選項(xiàng)姻几,可以選擇崩潰時(shí)間,版本號(hào),build版本等蛇捌。點(diǎn)擊一個(gè)崩潰項(xiàng)可以看到具體的崩潰線程以及堆棧信息抚恒。右側(cè)Crash Log Details
中展示了本次崩潰的項(xiàng)目名稱,build版本號(hào)络拌,手機(jī)系統(tǒng)俭驮,手機(jī)型號(hào)等信息。下方Statistics
顯示了因?yàn)樵撛虮罎⒌脑O(shè)備數(shù)量春贸,開(kāi)發(fā)人員可通過(guò)崩潰設(shè)備數(shù)量來(lái)制定修復(fù)優(yōu)先級(jí)混萝。
??如果你在發(fā)布App時(shí)上傳了dSYM文件,當(dāng)你使用<font color=red>【還需要驗(yàn)證】crashes工具
</font>時(shí)就會(huì)自動(dòng)符號(hào)化崩潰信息萍恕,并且可以通過(guò)符號(hào)化后的信息快速定位到崩潰代碼逸嘀,我們可以通過(guò)點(diǎn)擊崩潰堆棧右側(cè)的小箭頭或Open in Project...
并在彈出的選項(xiàng)中選擇對(duì)應(yīng)的項(xiàng)目名稱即可定位到具體的錯(cuò)誤代碼。
??有時(shí)崩潰定位不準(zhǔn)確或崩潰信息未符號(hào)化允粤,我們可以通過(guò)崩潰日志手動(dòng)進(jìn)行符號(hào)化崭倘。只需要在崩潰堆棧處右鍵選擇Show in Finder
即可定位到crash
崩潰文件。具體符號(hào)化崩潰信息的方式在下一節(jié)类垫。
??3.使用代碼監(jiān)聽(tīng)
收集:
??使用代碼監(jiān)聽(tīng)
是通過(guò)代碼注冊(cè)異乘竟猓回調(diào)。當(dāng)應(yīng)用程序發(fā)生異常時(shí)優(yōu)先將異常信息保存在本地悉患,并在適當(dāng)?shù)臅r(shí)機(jī)上傳給開(kāi)發(fā)人員飘庄。該方式需要先將開(kāi)發(fā)模式改為Release
模式,并且在Edit Scheme
中關(guān)閉Debug executable
選項(xiàng)购撼。否則在應(yīng)用發(fā)生崩潰時(shí)會(huì)優(yōu)先被Xcode攔截跪削,不會(huì)走監(jiān)聽(tīng)方法。
??在OC項(xiàng)目中可以使用NSSetUncaughtExceptionHandler()
方法進(jìn)行異常監(jiān)聽(tīng)迂求,例如當(dāng)程序出現(xiàn)崩潰時(shí)該方法會(huì)在Block中傳入NSException
參數(shù)碾盐,通過(guò)打印可以發(fā)現(xiàn)NSException
中包含了崩潰時(shí)的錯(cuò)誤堆棧信息,以及崩潰原因等數(shù)據(jù)揩局。
??在Swift項(xiàng)目中毫玖,需要使用objc_setUncaughtExceptionHandler()
來(lái)替代NSSetUncaughtExceptionHandler()
方法,并且該方法只有在OC代碼產(chǎn)生異沉瓒ⅲ或崩潰時(shí)才會(huì)調(diào)用付枫。而Swift代碼異常需要使用signal()
監(jiān)聽(tīng)注冊(cè)異常信號(hào),該方法也會(huì)同時(shí)監(jiān)聽(tīng)OC代碼的崩潰與異常驰怎。
// 崩潰代碼
+ (void) testArrayError {
NSArray *ary = [NSArray array];
ary[10];
}
// 監(jiān)測(cè)代碼
objc_setUncaughtExceptionHandler { exception in
let exception = exception as? NSException
for text in exception?.callStackSymbols ?? [] {
print(text)
}
print(exception?.name)
print(exception?.reason)
}
// 崩潰信息打印
============================================================================
0 CoreFoundation 0x00000001badbd29c 5198FB57-5645-3B34-A49F-F32B52256CF3 + 627356
1 libobjc.A.dylib 0x00000001d3ab7744 objc_exception_throw + 60
2 CoreFoundation 0x00000001bae455c4 5198FB57-5645-3B34-A49F-F32B52256CF3 + 1185220
3 ErrorDemo 0x00000001041bd804 +[TestObject testArrayError] + 60
4 ErrorDemo 0x00000001041be708 $sIeg_IeyB_TR + 28
5 libdispatch.dylib 0x00000001baa22e6c 355ACCF4-3917-3730-BC55-EF7003887ABE + 7788
6 libdispatch.dylib 0x00000001baa24a30 355ACCF4-3917-3730-BC55-EF7003887ABE + 14896
7 libdispatch.dylib 0x00000001baa27b44 355ACCF4-3917-3730-BC55-EF7003887ABE + 27460
8 libdispatch.dylib 0x00000001baa36164 355ACCF4-3917-3730-BC55-EF7003887ABE + 86372
9 libdispatch.dylib 0x00000001baa3696c 355ACCF4-3917-3730-BC55-EF7003887ABE + 88428
10 libsystem_pthread.dylib 0x000000022c680080 _pthread_wqthread + 228
11 libsystem_pthread.dylib 0x000000022c67fe5c start_wqthread + 8
============================================================================
Optional(__C.NSExceptionName(_rawValue: NSRangeException))
============================================================================
Optional("*** -[__NSArray0 objectAtIndex:]: index 10 beyond bounds for empty NSArray")
libc++abi: terminating with uncaught exception of type NSException
??使用signal()
可以注冊(cè)多種信號(hào)阐滩,具體的信號(hào)列表可以通過(guò)Darwin.sys.signal
文件進(jìn)行查看,其中比較常用的信號(hào)為SIGTRAP
县忌,在常規(guī)造成的異车嗬疲或崩潰中一般都會(huì)觸發(fā)SIGTRAP
信號(hào)继效。
??signal()
方法需要傳入兩個(gè)參數(shù),第一個(gè)參數(shù)為信號(hào)標(biāo)識(shí)装获,例如SIGTRAP
這種瑞信。第二個(gè)參數(shù)為觸發(fā)異常信號(hào)時(shí)的回調(diào),由于是C語(yǔ)言閉包在使用時(shí)需要增加@convention(c)
標(biāo)識(shí)穴豫,之后在閉包中獲取異常堆棧結(jié)構(gòu)樹(shù)并打印凡简。
// 注冊(cè)監(jiān)聽(tīng)
signal(SIGTRAP, handleSignalException)
// 閉包監(jiān)聽(tīng)
let handleSignalException: @convention(c) (Int32) -> Void = { value in
let pointer = UnsafeMutablePointer<UnsafeMutableRawPointer?>.allocate(capacity: 128)
// backtrace 兩個(gè)方法需要橋接導(dǎo)入頭文件 #import <execinfo.h>
let frame = backtrace(pointer, 128)
let strs = backtrace_symbols(pointer, frame)
for i in 0 ..< Int(frame) {
let str = strs?[Int(i)]
print(NSString.init(cString: str!, encoding: String.Encoding.utf8.rawValue) ?? "")
}
free(strs)
// 在監(jiān)聽(tīng)完成后需要取消監(jiān)聽(tīng),否則主線程崩潰的App會(huì)一直處于卡死狀態(tài)精肃,不會(huì)退出秤涩。
signal(SIGTRAP, SIG_DFL)
}
// 打印內(nèi)容
============================================================================
0 ErrorDemo 0x000000010227baa8 $s9ErrorDemo11AppDelegateC08registerA0yyFys5Int32VcfU0_Tf4d_n + 108
1 libsystem_platform.dylib 0x000000022c678c10 68A13C2E-80DD-3754-B166-C0A5992A7417 + 7184
2 ??? 0xffffff810227ab98 0x0 + 18446743528284859288
3 ErrorDemo 0x000000010227a468 $s9ErrorDemo14ViewControllerC05tableC0_14didSelectRowAtySo07UITableC0C_10Foundation9IndexPathVtFTo + 136
4 UIKitCore 0x00000001be258518 3ED35565-456D-33CB-B554-6C567FA81585 + 17540376
5 UIKitCore 0x00000001be257ea8 3ED35565-456D-33CB-B554-6C567FA81585 + 17538728
6 UIKitCore 0x00000001be258798 3ED35565-456D-33CB-B554-6C567FA81585 + 17541016
7 UIKitCore 0x00000001bd4c64dc 3ED35565-456D-33CB-B554-6C567FA81585 + 3310812
8 UIKitCore 0x00000001bd3d9864 3ED35565-456D-33CB-B554-6C567FA81585 + 2340964
9 UIKitCore 0x00000001bd303074 3ED35565-456D-33CB-B554-6C567FA81585 + 1462388
10 UIKitCore 0x00000001bd303798 3ED35565-456D-33CB-B554-6C567FA81585 + 1464216
11 UIKitCore 0x00000001bd6dc1a4 3ED35565-456D-33CB-B554-6C567FA81585 + 5497252
12 UIKitCore 0x00000001bd97697c 3ED35565-456D-33CB-B554-6C567FA81585 + 8227196
13 UIKitCore 0x00000001bdffcc48 3ED35565-456D-33CB-B554-6C567FA81585 + 15068232
14 UIKitCore 0x00000001bdffc410 3ED35565-456D-33CB-B554-6C567FA81585 + 15066128
15 CoreFoundation 0x00000001baddf414 5198FB57-5645-3B34-A49F-F32B52256CF3 + 766996
16 CoreFoundation 0x00000001badf01a0 5198FB57-5645-3B34-A49F-F32B52256CF3 + 836000
17 CoreFoundation 0x00000001bad29694 5198FB57-5645-3B34-A49F-F32B52256CF3 + 22164
18 CoreFoundation 0x00000001bad2f05c 5198FB57-5645-3B34-A49F-F32B52256CF3 + 45148
19 CoreFoundation 0x00000001bad42bc8 CFRunLoopRunSpecific + 600
20 GraphicsServices 0x00000001d6e76374 GSEventRunModal + 164
21 UIKitCore 0x00000001bd6b2648 3ED35565-456D-33CB-B554-6C567FA81585 + 5326408
22 UIKitCore 0x00000001bd433d90 UIApplicationMain + 364
23 libswiftUIKit.dylib 0x00000001d2aafecc $s5UIKit17UIApplicationMainys5Int32VAD_SpySpys4Int8VGGSgSSSgAJtF + 104
24 ErrorDemo 0x000000010227a5e0 main + 108
25 dyld 0x0000000102835ce4 start + 520
??從打印內(nèi)容可以看出,在ErrorDemo
項(xiàng)中也包含了未還原的堆棧名稱肋杖,并且該內(nèi)容與最開(kāi)始Bugly
中未還原的堆棧名稱類似(因?yàn)槎际窃邳c(diǎn)擊列表項(xiàng)時(shí)觸發(fā)的崩潰)溉仑。由此可以得出Bugly
也抓取到了類似的信息挖函,只是暫時(shí)還無(wú)法得知Bugly
是否使用該方式收集的異常信息状植,以及它是如何從該信息中解析出數(shù)組越界等崩潰的具體原因。
??除此之外怨喘,我們?cè)趶牡谌綆?kù)中可以看到崩潰信息的組成中包含了多個(gè)線程的堆棧信息津畸。而我們使用的該方法目前只能打印出觸發(fā)異常線程的堆棧信息,所以我認(rèn)為還有其他方法能拿到其他線程的信息必怜,在查閱其他文章后找到了最有可能的文件 -- Darwin.Mach.task
肉拓。
??從文件名可以看出,該文件屬于Mach
③線程文件梳庆,其中有一個(gè)方法名為task_threads()
暖途,該方法的功能為獲取所有的線程列表,正符合目前獲取多個(gè)線程信息的需求膏执。不過(guò)由于時(shí)間原因驻售,對(duì)崩潰收集的調(diào)研只能先暫停在這里,該文件是對(duì)多線程崩潰信息收集很好的一個(gè)切入點(diǎn)更米,后邊可以沿著這條線繼續(xù)摸索下去欺栗。當(dāng)然,閱讀第三方庫(kù)的源碼會(huì)是更好的選擇征峦。
四迟几、崩潰信息符號(hào)化
// 在我們收集的崩潰信息中,未符號(hào)化前的堆棧為十六進(jìn)制碼栏笆。
// 例如:
0 ErrorDemo 0x0000000100be8234 0x100be0000 + 33332
??符號(hào)化的方式有三種:
??第一種為symbolicatecrash
:該方式可以將完整的崩潰信息符號(hào)化类腮,但是會(huì)出現(xiàn)自己項(xiàng)目的堆棧信息未符號(hào)化的情況,iOS14及以下系統(tǒng)可用蛉加。
??第二種為atos
:該方式可以只將某一條堆棧單獨(dú)符號(hào)化存哲,但是只能符號(hào)化自己項(xiàng)目的堆棧因宇,無(wú)法符號(hào)化系統(tǒng)堆棧,沒(méi)有系統(tǒng)限制祟偷。
??第三種為CrashSymbolicator.py
:可以將完整的崩潰信息全部符號(hào)化(包括系統(tǒng)堆棧與項(xiàng)目堆棧)察滑,并且得到的崩潰信息比symbolicatecrash
更詳細(xì),iOS15及以上系統(tǒng)可用修肠。
??注:三種方式都需要與之對(duì)應(yīng)的dSYM文件贺辰,由于每次編譯都會(huì)創(chuàng)建新的dSYM文件,所以一定要在編譯或打包后將dSYM文件存好嵌施。并且不同系統(tǒng)的崩潰信息需要使用對(duì)應(yīng)的方法進(jìn)行符號(hào)化饲化,否則可能會(huì)出現(xiàn)失敗的情況。
??dSYM文件的獲取方式:
??對(duì)于未打包的項(xiàng)目
:獲取dSYM文件可以通過(guò)項(xiàng)目目錄中的Products
-> [項(xiàng)目名稱].app
-> Show in Finder
即可找到[項(xiàng)目名稱].app.DSYM
文件吗伤。
??對(duì)于打包完成的項(xiàng)目
:通過(guò)對(duì)打包后的Archives
項(xiàng)目Show in Finder
-> 顯示包內(nèi)容
-> dSYMs
即可找到[項(xiàng)目名稱].app.DSYM
文件吃靠。
??這里需要注意的是默認(rèn)情況下只有Release
模式才產(chǎn)生dSYM文件。如果想要Debug
模式下也產(chǎn)生dSYM文件的話足淆,需要在Build Settings
-> Debug Information Format
中將DWARF
改為DWARF with dSYM File
巢块。
??1.使用symbolicatecrash
符號(hào)化(iOS14及以下系統(tǒng)可用):
??首先我們需要找到symbolicatecrash
文件:
// 該文件一般在該路徑下
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash
// 如果在上面路徑找不到的話可以通過(guò)命令進(jìn)行查找。
find /Applications/Xcode.app -name symbolicatecrash -type f
??在找到symbolicatecrash
文件后巧号,隨意新建一個(gè)文件夾族奢。將該文件copy一份與dSYM文件和crash文件同放在新建的文件夾內(nèi)。
// 首先打開(kāi) .bash_profile 文件丹鸿,添加環(huán)境變量
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
// 保存并執(zhí)行命令
source ~/.bash_profile
// 然后cd到新建的文件夾內(nèi)
cd xxx
// 使用命令進(jìn)行符號(hào)化
./symbolicatecrash [crash文件或ips文件] [dSYM文件] > [符號(hào)化后的新文件名稱.crash]
// 例如
./symbolicatecrash ErrorTest.crash ErrorTest.app.dSYM > newCrash.crash
// 符號(hào)化后的部分信息
============================================================================
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 ErrorTest 0x0000000100b64e50 0x100b60000 + 20048
1 UIKit 0x000000018bf3183c forwardTouchMethod + 340
2 UIKit 0x000000018bdd7760 -[UIResponder touchesBegan:withEvent:] + 60
3 UIKit 0x000000018bdd17c8 -[UIWindow _sendTouchesForEvent:] + 1892
4 UIKit 0x000000018bdc6890 -[UIWindow sendEvent:] + 3160
5 UIKit 0x000000018bdc51d0 -[UIApplication sendEvent:] + 340
6 UIKit 0x000000018c5a6d1c __dispatchPreprocessedEventFromEventQueue + 2340
7 UIKit 0x000000018c5a92c8 __handleEventQueueInternal + 4744
8 UIKit 0x000000018c5a2368 __handleHIDEventFetcherDrain + 152
9 CoreFoundation 0x0000000181f8b404 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
10 CoreFoundation 0x0000000181f8ac2c __CFRunLoopDoSources0 + 276
11 CoreFoundation 0x0000000181f8879c __CFRunLoopRun + 1204
12 CoreFoundation 0x0000000181ea8da8 CFRunLoopRunSpecific + 552
13 GraphicsServices 0x0000000183e8d020 GSEventRunModal + 100
14 UIKit 0x000000018bec5758 UIApplicationMain + 236
15 libswiftUIKit.dylib 0x0000000100c9d468 0x100c94000 + 37992
16 ErrorTest 0x0000000100b65004 0x100b60000 + 20484
17 libdyld.dylib 0x0000000181939fc0 start + 4
??從符號(hào)化后的堆棧信息中可以發(fā)現(xiàn)ErrorTest
項(xiàng)目堆棧并未符號(hào)化越走,需要再次使用atos
進(jìn)行符號(hào)化。
??如果執(zhí)行命令報(bào)錯(cuò):
??錯(cuò)誤一:
Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash line 75.
// 需要設(shè)置環(huán)境變量靠欢,并執(zhí)行 source ~/.bash_profile
??錯(cuò)誤二:
No crash report version in [文件名].crash at ./symbolicatecrash line 1371.
// 該崩潰信息可能需要使用 CrashSymbolicator.py 進(jìn)行解析
??2.使用atos
符號(hào)化(無(wú)系統(tǒng)限制):
# 例如
0 ErrorDemo 0x1001f4660 0x1001ec000 + 34400
1 ErrorDemo 0x1001f4504 0x1001ec000 + 34052
2 ErrorDemo 0x1001f41c8 0x1001ec000 + 33224
# 注意這里的路徑為dSYM文件顯示包內(nèi)容后的項(xiàng)目路徑
atos -o /ErrorDemo.app.dSYM/Contents/Resources/DWARF/ErrorDemo -l 0x1001ec000 0x1001f4660
# 其中 0x1001ec000 為起始地址廊敌,0x1001f4660為偏移地址。注意順序C殴帧B獬骸!
# 符號(hào)化后
specialized ViewController.tableView(_:didSelectRowAt:) (in ErrorDemo) (ViewController.swift:66)
??3.使用CrashSymbolicator.py
符號(hào)化(iOS15及以上系統(tǒng)可用):
??該方式與symbolicatecrash
方式類似薪缆,首先需要找到CrashSymbolicator.py
文件:
# 該文件一般在該路徑下
/Applications/Xcode.app/Contents/SharedFrameworks/CoreSymbolicationDT.framework/Versions/A/Resources/CrashSymbolicator.py
# 如果在上面路徑找不到的話可以通過(guò)命令進(jìn)行查找秧廉。
find /Applications/Xcode.app -name CrashSymbolicator -type f
??在找到該文件后不要
將它c(diǎn)opy到其他文件夾內(nèi),否則會(huì)出現(xiàn)找不到頭文件的問(wèn)題拣帽。
# 執(zhí)行命令
python3 [CrashSymbolicator.py文件路徑] -d [dSYM文件路徑] -o [符號(hào)化后的新文件名稱.ips] -p [崩潰信息ips文件路徑]
# 例如
python3 /Applications/Xcode.app/Contents/SharedFrameworks/CoreSymbolicationDT.framework/Versions/A/Resources/CrashSymbolicator.py -d /Users/kenan/Desktop/15.0.2/ErrorDemo.app.dSYM -o newCrash.ips -p /Users/kenan/Desktop/15.0.2/ErrorDemo.ips
# 符號(hào)化后的部分信息
============================================================================
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 ErrorDemo 0x1001f4660 Swift runtime failure: Index out of range + 0 (ViewController.swift:0) [inlined]
1 ErrorDemo 0x1001f4660 specialized _ArrayBuffer._checkInoutAndNativeTypeCheckedBounds(_:wasNativeTypeChecked:) + 0 (<compiler-generated>:0) [inlined]
2 ErrorDemo 0x1001f4660 specialized Array._checkSubscript(_:wasNativeTypeChecked:) + 0 (<compiler-generated>:0) [inlined]
3 ErrorDemo 0x1001f4660 specialized Array.subscript.getter + 0 (<compiler-generated>:0) [inlined]
4 ErrorDemo 0x1001f4660 ViewController.arrayError() + 0 (ViewController.swift:84) [inlined]
5 ErrorDemo 0x1001f4660 specialized ViewController.tableView(_:didSelectRowAt:) + 456 (ViewController.swift:66)
6 ErrorDemo 0x1001f4504 specialized ViewController.tableView(_:didSelectRowAt:) + 107 (<compiler-generated>:0)
7 ErrorDemo 0x1001f41c8 ViewController.tableView(_:didSelectRowAt:) + 11 (<compiler-generated>:0) [inlined]
8 ErrorDemo 0x1001f41c8 @objc ViewController.tableView(_:didSelectRowAt:) + 135 (<
··· ···
24 ErrorDemo 0x1001f2c14 specialized static UIApplicationDelegate.main() + 79 (LoadAnimationView_01.swift:0) [inlined]
25 ErrorDemo 0x1001f2c14 static AppDelegate.$main() + 91 (<compiler-generated>:16) [inlined]
26 ErrorDemo 0x1001f2c14 main + 107 (LoadAnimationView_01.swift:0)
27 dyld 0x1005b4190 start + 443
??從符號(hào)化后的堆棧信息可以看出疼电,本次崩潰原因?yàn)?code>Swift runtime failure: Index out of range,崩潰的位置為ViewController.swift
文件减拭。崩潰的操作為ViewController.tableView(_:didSelectRowAt:)
蔽豺。
五、崩潰中斷攔截
??崩潰中斷攔截
是在代碼監(jiān)聽(tīng)
的基礎(chǔ)上拧粪,在崩潰前進(jìn)行某些操作修陡,完成后再退出應(yīng)用沧侥。有同學(xué)表示這種操作可能會(huì)導(dǎo)致APP崩潰后無(wú)法再次啟動(dòng),目前雖無(wú)法證實(shí)這種說(shuō)法魄鸦,但還是不太建議使用宴杀,該部分屬于擴(kuò)展知識(shí)。
??實(shí)現(xiàn)中斷攔截首先是注冊(cè)signal()
信號(hào)并實(shí)現(xiàn)它的閉包方法拾因,當(dāng)程序發(fā)生崩潰時(shí)會(huì)調(diào)用handleSignalException
閉包旺罢。這時(shí)可以將錯(cuò)誤堆棧打印到控制臺(tái),由于在代碼監(jiān)聽(tīng)
部分演示過(guò)绢记,這里就不再添加這部分代碼了扁达。
??這里我們?cè)贏ppDelegate外邊定義了一個(gè)全局變量appDelegate
,因?yàn)?code>handleSignalException是一個(gè)c
轉(zhuǎn)義閉包蠢熄,我們沒(méi)有辦法在其中直接獲取或使用局部變量跪解,所以需要定義一個(gè)全局變量進(jìn)行操作(也許還有其他方法可以實(shí)現(xiàn),如果你找到了請(qǐng)告訴我)签孔。
??之后我們?cè)陂]包中調(diào)用一個(gè)外部方法demo()
叉讥,在該方法里我們做一些其他操作。
// 全局變量
var appDelegate: AppDelegate?
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
// 狀態(tài)控制
var state = true
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
appDelegate = self
// 回調(diào)方法
let handleSignalException: @convention(c) (Int32) -> Void = { value in
appDelegate?.demo()
}
// 注冊(cè)各種信號(hào)
signal(SIGSEGV, handleSignalException)
signal(SIGFPE, handleSignalException)
signal(SIGBUS, handleSignalException)
signal(SIGPIPE, handleSignalException)
signal(SIGHUP, handleSignalException)
signal(SIGINT, handleSignalException)
signal(SIGQUIT, handleSignalException)
signal(SIGABRT, handleSignalException)
signal(SIGILL, handleSignalException)
signal(SIGTRAP, handleSignalException)
return true
}
/// 取消監(jiān)聽(tīng)
/// 如果是主線程崩潰骏啰,只有執(zhí)行了該方法應(yīng)用程序才會(huì)退出节吮,否則程序會(huì)一直處于卡死狀態(tài)抽高,會(huì)不斷的回調(diào) handleSignalException
func unRegisterError() {
signal(SIGSEGV, SIG_DFL)
signal(SIGFPE, SIG_DFL)
signal(SIGBUS, SIG_DFL)
signal(SIGPIPE, SIG_DFL)
signal(SIGHUP, SIG_DFL)
signal(SIGINT, SIG_DFL)
signal(SIGQUIT, SIG_DFL)
signal(SIGABRT, SIG_DFL)
signal(SIGILL, SIG_DFL)
signal(SIGTRAP, SIG_DFL)
}
func demo() {
// do something
}
}
??例如判耕,我們希望在應(yīng)用程序崩潰后直接將崩潰信息上傳到我們的服務(wù)器,在上傳完成后再退出應(yīng)用翘骂。
??首先我們需要獲取當(dāng)前崩潰線程的Runloop
以及它的所有Mode
壁熄,然后發(fā)送網(wǎng)絡(luò)請(qǐng)求,當(dāng)請(qǐng)求完成后更改state
的狀態(tài)碳竟。在此期間強(qiáng)制Runloop
運(yùn)行防止程序退出草丧,這時(shí)該線程會(huì)陷入死循環(huán),卡在While
的位置莹桅。一直等到state
狀態(tài)修改為false
昌执,才會(huì)執(zhí)行unRegisterError()
方法退出程序。
func demo() {
// 1. 獲取 Runloop 與 modes
let runloop = CFRunLoopGetCurrent()
let modes = CFRunLoopCopyAllModes(runloop) as? NSArray
// 2. 運(yùn)行網(wǎng)絡(luò)請(qǐng)求
var request = URLRequest(url: URL(string: "https://www.baidu.com/")!)
request.httpMethod = "GET"
let session = URLSession.shared
let task = session.dataTask(with: request) { data, response, error in
if let data = data {
print(String(data: data, encoding: .utf8))
}
// 請(qǐng)求完成修改狀態(tài)
self.state = false
}
task.resume()
// 3. 運(yùn)行防止程序退出
while (state) {
for model in modes ?? [] {
let current = model as? NSString ?? ""
CFRunLoopRunInMode(.init(rawValue:current), 0.001, false)
}
}
// 4. 取消監(jiān)聽(tīng)诈泼,退出應(yīng)用
unRegisterError()
}
??這樣我們就實(shí)現(xiàn)了在崩潰時(shí)進(jìn)行網(wǎng)絡(luò)請(qǐng)求的功能懂拾。不過(guò)由于當(dāng)前程序已經(jīng)崩潰,應(yīng)用也會(huì)卡死造成用戶操作無(wú)反應(yīng)铐达。這時(shí)過(guò)長(zhǎng)時(shí)間的操作可能會(huì)使用戶強(qiáng)制退出應(yīng)用岖赋,導(dǎo)致我們的操作都無(wú)法完成。所以除了網(wǎng)絡(luò)請(qǐng)求外瓮孙,我們也可以再崩潰時(shí)給用戶一個(gè)彈窗唐断,告訴用戶程序已經(jīng)崩潰正在收集崩潰信息选脊∪裱模或者我們也可以添加一個(gè)可以其他按鈕矮慕,點(diǎn)擊后退出也可以。
func demo() {
// 1. 獲取 Runloop 與 modes
let runloop = CFRunLoopGetCurrent()
let modes = CFRunLoopCopyAllModes(runloop) as? NSArray
// 2. 運(yùn)行網(wǎng)絡(luò)請(qǐng)求
var request = URLRequest(url: URL(string: "https://www.baidu.com/")!)
request.httpMethod = "GET"
let session = URLSession.shared
let task = session.dataTask(with: request) { data, response, error in
if let data = data {
print(String(data: data, encoding: .utf8))
}
// 請(qǐng)求完成修改狀態(tài)
self.state = false
}
task.resume()
// 2.1 顯示提示信息
let alertLabel = UILabel()
alertLabel.backgroundColor = .red
alertLabel.frame = .init(x: 100, y: 100, width: 100, height: 100)
alertLabel.text = "程序出現(xiàn)異常星瘾,正在收集錯(cuò)誤信息丹诀,請(qǐng)不要退出"
window?.addSubview(alertLabel)
// 3. 運(yùn)行防止程序退出
// 長(zhǎng)時(shí)間的while會(huì)導(dǎo)致 Cpu 發(fā)熱
while (state) {
for model in modes ?? [] {
let current = model as? NSString ?? ""
CFRunLoopRunInMode(.init(rawValue:current), 0.001, false)
}
}
// 4. 取消監(jiān)聽(tīng)角寸,退出應(yīng)用
unRegisterError()
}
??不過(guò)顯示UI的方法并不是所有手機(jī)都可以,經(jīng)過(guò)測(cè)試13 Pro 15.5
(能測(cè)到的最好的手機(jī)了) 的真機(jī)會(huì)出現(xiàn)View顯示不出來(lái)的情況忿墅,需要進(jìn)入后臺(tái)重新進(jìn)入應(yīng)用才會(huì)顯示彈窗扁藕。但是相同系統(tǒng)型號(hào)的模擬器就可以,其他幾個(gè)系統(tǒng)和不同型號(hào)的真機(jī)也都可以(13 Pro max 不清楚)疚脐。
??最開(kāi)始猜測(cè)是程序崩潰后刷新率變?yōu)?導(dǎo)致UI不刷新亿柑。然后在頁(yè)面上加了個(gè)持續(xù)旋轉(zhuǎn)的動(dòng)畫,發(fā)現(xiàn)崩潰后動(dòng)畫還在轉(zhuǎn)但是彈窗依然沒(méi)有出現(xiàn)棍弄,證明并不是屏幕沒(méi)有刷新望薄。所以目前也還沒(méi)有找到原因,感覺(jué)應(yīng)該和刷新率有關(guān)呼畸。不過(guò)這也只是擴(kuò)展知識(shí)痕支,不建議大家再項(xiàng)目中做過(guò)于復(fù)雜的操作,程序崩潰時(shí)本身就處于一個(gè)不穩(wěn)定的狀態(tài)蛮原,在做其他操作可能會(huì)導(dǎo)致意想不到的Bug卧须。
??以上就是iOS崩潰追蹤的全部?jī)?nèi)容了,涉及的內(nèi)容比較雜也并沒(méi)有研究很深的技術(shù)儒陨,了解一下就可以了花嘶。本文出于學(xué)習(xí)與記錄的目的,如有不嚴(yán)謹(jǐn)?shù)牡胤竭€請(qǐng)?zhí)岢鲂薷摹?/strong>
??????????如果覺(jué)得有用就點(diǎn)個(gè)贊吧蹦漠,你的贊是我最大的動(dòng)力??????????
解釋
① 符號(hào)化:符號(hào)化是指通過(guò)命令將dSYM文件與崩潰信息進(jìn)行對(duì)比與解析椭员,將十六進(jìn)制碼轉(zhuǎn)換為可讀的錯(cuò)誤代碼。
② dSYM:dSYM是App編譯后的符號(hào)表文件笛园,需要使用對(duì)應(yīng)的dSYM文件才可以解析錯(cuò)誤堆棧信息隘击。
③ Mach:Mach是一個(gè)用于支持操作系統(tǒng)研究的操作系統(tǒng)內(nèi)核,在iOS系統(tǒng)的內(nèi)核中混合使用了該內(nèi)核研铆。
參考
iOS/OSX Crash:崩潰日志報(bào)告
iOS 崩潰符號(hào)化工具- iOS 15 CrashSymbolicator
iOS獲取任意線程調(diào)用堆棧信息
iOS dSYM詳解和分析crash埋同,ips文件
iOS堆棧信息解析(函數(shù)地址與符號(hào)關(guān)聯(lián))
iOS Crash從捕獲到符號(hào)化解析分析
iOS中線程Call Stack的捕獲和解析(二)
iOS/OSX Crash:捕捉異常
iOS異常處理-signal信號(hào)