日常開放中,我們難免遇到一些 crash。大部分情況下,Xcode 可以幫助我們找到問題所在,但也有些情況,Xcode 給我們反饋的是一些看不懂的地址,大大增加了我們分析問題的難度。
下面,就來介紹幾種能讓看不懂的地址,變得看的懂的方式。
symbolicatecrash
.dSYM 檔案
dSYM 是儲存十六進位制函式地址對映資訊的中轉檔案,我們除錯的 symbols 都會包含在這個檔案中。每次編譯專案的時候都會生成一個新的 dSYM 檔案,我們應該儲存每個正式釋出版本的 dSYM 檔案,以備我們更好的除錯問題。一般是在我們 Archives 時儲存對應的版本檔案的,裡面也有對應的 .dSYM
和 .app
檔案。路徑為:
~/Library/Developer/Xcode/Archives
複製程式碼
.dSYM
檔案預設在 debug 模式下是不生成的,我們去 Build Settings -> Debug Information Format 下,將 DWARF
修改為 DWARF with dSYM File
,再重新編譯下就能生成 .dSYM
檔案了,直接去專案工程的 Products
目錄下找就行。
symbols 又是什麼呢?
引用 《程式設計師的自我修養》中的解釋:
在連結中,我們將函式和變數統稱為符號(Symbol),函式名或變數名就是符號名(Symbol Name)。我們可以將符號看作是連結中的粘合劑,整個連結過程正是基於符號才能夠正確完成。
所以,所謂的 symbols 就是函式名或變數名。
找到 symbolicatecrash
symbolicatecrash
是 Xcode 自帶的 crash 日誌分析工具,我們需要先找到它:
find /Applications/Xcode.app -name symbolicatecrash -type f
複製程式碼
執行完後會返回幾個路徑,我的是:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash
複製程式碼
我們到這個路徑下把 symbolicatecrash
拷貝出來,放到一個資料夾下。
拿到 crash 日誌檔案
我們可以隨便寫段強制 crash 的程式碼:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSArray *arr = @[];
arr[100];
}
複製程式碼
接著用真機打個包。打好包之後,不要用 Xcode build,直接用打好的包執行我們能導致 crash 的程式碼,這樣就生成好 .crash
日誌檔案了。
之後,我們去 Xcode -> Window -> Devices and Simulators 或者快捷鍵 Command + Shift + 2
找到對應時間點的 .crash 檔案,右擊 Export Log。
拿到 .app 檔案
.app
檔案可以使用真機編譯下,去 專案 Products
目錄下獲取,也可以去 Archives 目錄下獲取。
符號解析
利用 dSYM
將 .dSYM
、.crash
及 symbolicatecrash
放到同一個檔案下,執行命令:
./symbolicatecrash .crash檔案路徑 .dSYM檔案路徑 > 名字.crash
複製程式碼
利用 app
將 .app
、.crash
及 symbolicatecrash
放到同一個檔案下,執行命令:
./symbolicatecrash .crash檔案路徑 .app/appName 路徑 > 名字.crash
複製程式碼
可能會報錯誤:
Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash line 69.
複製程式碼
執行下命令就行:
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
複製程式碼
然後再重新生成下新的 .crash
檔案就行。
我們可以對比下沒有符號化和符號化的檔案,下面是我自己測試的例子,iPhone5 iOS 10.2
, 可能會有所不同:
Last Exception Backtrace:
0 CoreFoundation 0x1df60df2 __exceptionPreprocess + 126
1 libobjc.A.dylib 0x1d1c3072 objc_exception_throw + 33
2 CoreFoundation 0x1dee62f2 -[__NSArray0 objectAtIndex:] + 105
3 DreamDemo 0x0008088e 0x7c000 + 18574
4 UIKit 0x2319eb44 forwardTouchMethod + 289
5 UIKit 0x2319ea10 -[UIResponder touchesBegan:withEvent:] + 29
6 UIKit 0x23041c58 -[UIWindow _sendTouchesForEvent:] + 1599
7 UIKit 0x2303ca62 -[UIWindow sendEvent:] + 2657
8 UIKit 0x2300d870 -[UIApplication sendEvent:] + 315
9 UIKit 0x237a8998 __dispatchPreprocessedEventFromEventQueue + 2615
10 UIKit 0x237a25de __handleEventQueue + 829
11 CoreFoundation 0x1df1c716 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 7
12 CoreFoundation 0x1df1c220 __CFRunLoopDoSources0 + 433
13 CoreFoundation 0x1df1a4f6 __CFRunLoopRun + 757
14 CoreFoundation 0x1de6952e CFRunLoopRunSpecific + 481
15 CoreFoundation 0x1de6933c CFRunLoopRunInMode + 99
16 GraphicsServices 0x1f640bf8 GSEventRunModal + 151
17 UIKit 0x230789a2 -[UIApplication _run] + 569
18 UIKit 0x230730cc UIApplicationMain + 145
19 DreamDemo 0x000834cc 0x7c000 + 29900
20 libdyld.dylib 0x1d633506 _dyld_process_info_notify_release + 23
複製程式碼
問題也能看出來,但是因為第三行(DreamDemo)並沒有符號化,導致我們並不確定具體呼叫位置。
再來看看符號化之後的:
Last Exception Backtrace:
0 CoreFoundation 0x1df60df2 __exceptionPreprocess + 126
1 libobjc.A.dylib 0x1d1c3072 objc_exception_throw + 33
2 CoreFoundation 0x1dee62f2 -[__NSArray0 objectAtIndex:] + 105
3 DreamDemo 0x0008088e -[ViewController touchesBegan:withEvent:] + 18574 (ViewController.m:84)
4 UIKit 0x2319eb44 forwardTouchMethod + 289
5 UIKit 0x2319ea10 -[UIResponder touchesBegan:withEvent:] + 29
6 UIKit 0x23041c58 -[UIWindow _sendTouchesForEvent:] + 1599
7 UIKit 0x2303ca62 -[UIWindow sendEvent:] + 2657
8 UIKit 0x2300d870 -[UIApplication sendEvent:] + 315
9 UIKit 0x237a8998 __dispatchPreprocessedEventFromEventQueue + 2615
10 UIKit 0x237a25de __handleEventQueue + 829
11 CoreFoundation 0x1df1c716 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 7
12 CoreFoundation 0x1df1c220 __CFRunLoopDoSources0 + 433
13 CoreFoundation 0x1df1a4f6 __CFRunLoopRun + 757
14 CoreFoundation 0x1de6952e CFRunLoopRunSpecific + 481
15 CoreFoundation 0x1de6933c CFRunLoopRunInMode + 99
16 GraphicsServices 0x1f640bf8 GSEventRunModal + 151
17 UIKit 0x230789a2 -[UIApplication _run] + 569
18 UIKit 0x230730cc UIApplicationMain + 145
19 DreamDemo 0x000834cc main + 29900 (main.m:15)
20 libdyld.dylib 0x1d633506 _dyld_process_info_notify_release + 23
複製程式碼
可以發現,第三行被解析出來了,這樣我們就能很清晰的知道具體的頁面了。
使用命令列工具 atos
symbolicatecrash 可以幫助我們很好的分析 crash 日誌,但是有它的侷限性 --- 不夠靈活。我們需要 symbolicatecrash
、.crash
及 .dSYM
三個檔案才能解析。
相對於 symbolicatecrash, atos
命令更加靈活,特別是你需要對不同渠道的 crash 檔案,寫一個自動化的分析指令碼的時候,這個方法就極其有用。
但是這種方式也有個不方便的地方:一個線上的 App,使用者使用的版本存在差異,而每個版本所對應的 .dSYM
都是不同的。必須確保 .crash
和 .dSYM
檔案是匹配的,才能正確符號化,匹配的條件就是它們的 UUID 一致。
在這之前,先介紹下 UUID:
什麼是 UUID ?
UUID 是由一組 32 位數的十六進位制數字所構成。每一個可執行程式都有一個 build UUID 唯一標識。.crash
日誌包含發生 crash 的這個應用的 build UUID 以及 crash 發生時,應用載入的所有庫檔案的 build UUID。
獲取 UUID
.crash UUID
執行命令:
grep --after-context=2 "Binary Images:" *crash
複製程式碼
輸出:
T.crash:Binary Images:
T.crash-0x7c000 - 0x87fff DreamDemo armv7 <d009f8671129397a8aab9cb2b8e506ff> /var/containers/Bundle/Application/DEEBE571-D512-4E8F-B712-ED4D19CE64F9/DreamDemo.app/DreamDemo
T.crash-0xa9000 - 0xd4fff dyld armv7s <cd60ff3403063c0aa8e54dff11e42527> /usr/lib/dyld
複製程式碼
看到上面的輸出 d009f8671129397a8aab9cb2b8e506ff
就是 DreamDemo
專案的 UUID。
.dSYM UUID
執行命令:
dwarfdump --uuid DreamDemo.app.dSYM
複製程式碼
輸出:
UUID: D009F867-1129-397A-8AAB-9CB2B8E506FF (armv7) DreamDemo.app.dSYM/Contents/Resources/DWARF/DreamDemo
複製程式碼
.app UUID
執行命令:
dwarfdump --uuid DreamDemo.app/DreamDemo
複製程式碼
輸出:
UUID: D009F867-1129-397A-8AAB-9CB2B8E506FF (armv7) DreamDemo.app/DreamDemo
複製程式碼
可以發現這兩個檔案的 UUID 是相同的,也就是匹配的,只有滿足這種條件,才能正確的解析!
atos 解析
我們現回顧下未解析前的堆疊:
2 CoreFoundation 0x1dee62f2 -[__NSArray0 objectAtIndex:] + 105
3 DreamDemo 0x0008088e 0x7c000 + 18574
4 UIKit 0x2319eb44 forwardTouchMethod + 289
5 UIKit 0x2319ea10 -[UIResponder touchesBegan:withEvent:] + 29
複製程式碼
執行命令:
xcrun atos -o DreamDemo.app.dSYM/Contents/Resources/DWARF/DreamDemo -arch armv7 -l 0x7c000
複製程式碼
接著輸入 0x0008088e
地址,終端輸出如下:
可以發現,正確的解析出來了!
除了搭配 .dSYM
檔案,我們也可以使用 .app
檔案來解析:
執行命令:
xcrun atos -o DreamDemo.app/DreamDemo -arch armv7 -l 0x7c000
複製程式碼
同樣輸入 0x0008088e
地址,效果是一樣的。
工具
直接操作 atos
命令畢竟是有點不方便,GitHub 上有個工具,可以輔助我們解析 dSYMTools ,這是個 Mac 客戶端,介面長這樣:
使用起來也很方便,我們只需要把對應的 dSYM
檔案拖進去,它會自動識別 UUID。我們對應的輸入引數地址就行:
系統庫的符號化解析
細心的人可以發現,我們上面的解析都是針對 DreamDemo
,這個自己的專案的。其實很多系統方法的堆疊之所以能解析出來,是因為已經有了系統庫的符號化檔案,存放目錄如下:
/使用者/使用者名稱稱xxx/資源庫/Developer/Xcode/iOS DeviceSupport
複製程式碼
這些庫的版本都是和 .crash
檔案中是對應的:
OS Version: iPhone OS 10.2 (14C5077b)
複製程式碼
一旦我把這個 10.2 (14C5077b)
系統的符號化庫刪掉,.crash
檔案就會變成這樣:
Last Exception Backtrace:
0 CoreFoundation 0x1df60df2 0x1de5f000 + 1056242
1 libobjc.A.dylib 0x1d1c3072 0x1d1bc000 + 28786
2 CoreFoundation 0x1dee62f2 0x1de5f000 + 553714
3 DreamDemo 0x000bc66e -[ViewController touchesBegan:withEvent:] + 18030 (ViewController.m:78)
4 UIKit 0x2319eb44 0x22ffe000 + 1706820
5 UIKit 0x2319ea10 0x22ffe000 + 1706512
6 UIKit 0x23041c58 0x22ffe000 + 277592
7 UIKit 0x2303ca62 0x22ffe000 + 256610
8 UIKit 0x2300d870 0x22ffe000 + 63600
9 UIKit 0x237a8998 0x22ffe000 + 8038808
10 UIKit 0x237a25de 0x22ffe000 + 8013278
11 CoreFoundation 0x1df1c716 0x1de5f000 + 775958
12 CoreFoundation 0x1df1c220 0x1de5f000 + 774688
13 CoreFoundation 0x1df1a4f6 0x1de5f000 + 767222
14 CoreFoundation 0x1de6952e 0x1de5f000 + 42286
15 CoreFoundation 0x1de6933c 0x1de5f000 + 41788
16 GraphicsServices 0x1f640bf8 0x1f637000 + 39928
17 UIKit 0x230789a2 0x22ffe000 + 502178
18 UIKit 0x230730cc 0x22ffe000 + 479436
19 DreamDemo 0x000bf332 main + 29490 (main.m:15)
20 libdyld.dylib 0x1d633506 0x1d630000 + 13574
複製程式碼
可以明顯的發現,系統庫的堆疊變成了一堆地址。
新版本,每當我們手機連上 Xcode 時,都會把當前版本的系統符號庫自動匯入到 /使用者/使用者名稱稱xxx/資源庫/Developer/Xcode/iOS DeviceSupport
目錄下。但是 iOS 版本那麼多,之前舊的系統符號庫該怎麼獲取呢?有人已經整理好了 iOS-System-Symbols,我們只需要根據 .crash
檔案的版本資訊,下載對應的系統符號化檔案到目錄下即可。
總結
- 利用 symbolicatecrash 解析,可以將整個
.crash
日誌堆疊解析,但是由於依賴symbolicatecrash
、.crash
以及.dSYM
三個檔案,或者.app
、.crash
及symbolicatecrash
三個檔案,導致不太靈活。 - 利用
atos
命令只需要.crash
和.dSYM
,或者.crash
和.app
,知道對應的堆疊地址,就能解析,方便自動化指令碼分析,但是 crash 堆疊可能需要自己實現收集。