這篇文章主要整理了crash log的符號化解析和調試信息與配置相關的一些內容。
對於做移動App開發的來說,質量和體驗都是很重要的。一個客戶端應用如果經常“閃退”,是產品質量很差的一個體現,用戶體驗就更不用提了。所以開發一個優秀的App,首先是保證自身的技術質量,盡量杜絕“閃退”,也就是“Crash”。但客戶端上線後,偶爾出現一個隱藏很深的bug也在所難免。我們所能做的就是盡可能的收集問題相關的信息,爭取在將來的新版本中解決和改進。
0. Crash
一個App啟動之後,用著用著就突然被iOS系統關閉,或者干脆就起不來,在打開的一瞬間關閉,這就是Crash,俗稱“閃退”“崩潰”。
iOS上的App閃退有各種各樣的原因,手機過熱、響應超時、內存過低都是有可能的crash原因。但更多情況下是App程序自身的運行邏輯存在問題、缺陷。比如調用用了Objective-C對象根本不支持的方法(發送消息),非法內存訪問,數組越界,參數不符合要求等。
這些問題在調試階段,我們都可以很容易的通過斷點和console中提供的信息快速定位並解決。
但對於已發布的App,如果想重現並利用上述辦法來解決,恐怕會比較費時費事。
最有幫助最直接的辦法就是根據出現問題時的閃退日志,分析和判斷crash的原因,快速准確的定位和解決。
1. Crash log
在iOS上運行的App出現crash的時候,通常會生成一個crash log,記載問題發生時的具體狀況。開發者可以在iTunes Connect(相當於App Store後台)中特定App下找到收集上來的crash log。不過客戶端用戶可以選擇不發送診斷信息,這樣收集上來的信息就不一定是全面的。
不過開發者可以對exception和signal設置自定義的handler做額外處理,以收集現場信息。現在也有很多第三方的工具很流行,比如Crashlytics,國內的友盟等。
閃退日志裡面包含了Crash發生的App、運行軟硬件環境、發生時間、錯誤類型、方法調用異常棧、各線程狀態、寄存器和內存信息。
而其中對我們開發人員來說意義最為重大的,可能就是異常線程的調用棧,例如:
Last Exception Backtrace: 0 CoreFoundation 0x18517e950 __exceptionPreprocess + 132 1 libobjc.A.dylib 0x1916841fc objc_exception_throw + 60 2 CoreFoundation 0x185085910 -[__NSDictionaryM setObject:forKey:] + 900 3 CrashDebugInfoTest 0x1000c2b90 0x1000bc000 + 27536 4 CrashDebugInfoTest 0x1000c28dc 0x1000bc000 + 26844 5 UIKit 0x1881bc55c -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 316 6 UIKit 0x1881bbf08 -[UIApplication _callInitializationDelegatesForURL:payload:suspended:] + 1564 7 UIKit 0x1881b59ec -[UIApplication _runWithURL:payload:launchOrientation:statusBarStyle:statusBarHidden:] + 772 8 UIKit 0x1881498cc -[UIApplication handleEvent:withNewEvent:] + 3316 9 UIKit 0x188148ad0 -[UIApplication sendEvent:] + 104 10 UIKit 0x1881b5044 _UIApplicationHandleEvent + 672 11 GraphicsServices 0x18ad63504 _PurpleEventCallback + 676 12 GraphicsServices 0x18ad63030 PurpleEventCallback + 48 13 CoreFoundation 0x18513e890 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 56 14 CoreFoundation 0x18513e7f0 __CFRunLoopDoSource1 + 444 15 CoreFoundation 0x18513ca14 __CFRunLoopRun + 1620 16 CoreFoundation 0x18507d6d0 CFRunLoopRunSpecific + 452 17 UIKit 0x1881b41c8 -[UIApplication _run] + 784 18 UIKit 0x1881aefdc UIApplicationMain + 1156 19 CrashDebugInfoTest 0x1000c2c5c 0x1000bc000 + 27740 20 libdyld.dylib 0x191c77aa0 start + 4
其中從第二列來看,很多是開發庫中的調用,而關鍵在於其間我們自己的App方法調用。可惜有些時候,這關鍵的信息竟然全是16進制的數據,我們很難看懂。比如:
3 CrashDebugInfoTest 0x1000c2b90 0x1000bc000 + 27536
那麼要從十六進制的地址碼,得到我們代碼中對應的方法調用,就需要結合調試信息對crash log進行符號化。
2. 符號化的各種方法
符號化的方法多種多樣,從網上社區論壇和個人經驗看來,至少有如下辦法:
使用開發工具庫中自帶的symbolicatecrash
使用atos
使用dwarfdump
更有牛人,自己寫了個復雜的腳本來解決這個問題。下面我介紹我常使用的兩種方法,一個是利用atos,一個是充分利用Xcode自帶的工具。其它的大家都可以到網上參看相關文章,一搜一大筐。
atos,就是address to symbol,把地址翻譯成符號。上面那段我提到了,要想把十六進制的地址翻譯為符號,需要調試信息。最好用的調試信息就是我們在每次給App打包時生成的dSYM文件。而atos最好用的方式就是:
atos -o XXX.app.dSYM/Contents/Resources/DWARF/XXX -l address0 targetAddress
其中:
XXX是AppName
address0是當前進程在內存中加載的起始地址,至於為什麼需要這個,那就有必要去了解下ASLR
targetAddress就是你想要符號化的地址了,比如0x1000c2b90
除了atos外,我想介紹的另一個辦法就是使用Xcode自帶的crash log分析工具,在老版本的Xcode中是在Organizer裡,在新版本裡是在Devices中。
有的朋友可能會說,那裡面顯示的可還是十六進制的地址啊!那是因為它“沒看到”App和dSYM文件啊。那怎麼辦?簡單:
把App和dSYM放在一個目錄中,並用mdimport把目錄加入到Spotlight的索引中即可。
怎麼樣,這招是不是更快更好用?symbolicatecrash神馬的就不需要了吧!
3. 針對framework靜態庫的crash定位和調試選項設置
之前本人曾經以framework(iOS Universal Framework)的方式開發了好多SDK供別人用。可當使用了framework庫的App閃退了的時候,即使是SDK中的邏輯問題,異常棧中顯示的也是App的名字。
更重要的是,默認情況下,異常棧的最右一列根本沒法符號化。
這是因為framework實際上是一種靜態庫,在Build App時,它已經完全“融入”了,靜態鏈接到App產物中。而在framework生成的時候,調試信息已經被抽取掉了。
我們打開SDK的工程文件,在Build Settings裡搜索Strip,會發現有好幾個選項:
Strip Debug Symbol During Copy
Strip Linked Product
Strip Style
Use Separate Strip
對於這個問題,我們只要在Strip Linked Product一項中選擇No就行了。這樣在Build出的SDK framework中,包的體積會變大,因為它容納了本要去除掉的調試信息。
按我在之前的Blog的辦法,我們看看在Mach-O文件中多了什麼:
Debug Info
是的,正是DWARF格式的數據。DWARF是一種通用的調試信息格式,可以認為是Debugging With Attributed Records Format的縮寫。感興趣的可以前往: http://www.dwarfstd.org
這樣,關於Crash問題的解決方案和原理我就解釋清楚了,歡迎大家拍磚!
(本文作者:三石)