Objective-C中的符号化崩溃日志分析实战
一、符号化崩溃日志的重要性
在Objective - C开发中,当应用程序发生崩溃时,系统会生成崩溃日志。这些日志对于定位和修复问题至关重要。然而,默认情况下,崩溃日志中的函数调用栈信息是以地址形式呈现的,这对于开发者来说很难直接理解和分析。
例如,下面是一段未符号化的崩溃日志中的调用栈片段:
Thread 0 Crashed:
0 MyApp 0x000f26a8 -[ViewController viewDidLoad] + 144
1 UIKitCore 0x187569c24 -[UIViewController loadViewIfRequired] + 204
2 UIKitCore 0x187569d48 -[UIViewController view] + 20
3 UIKitCore 0x187572670 -[UIViewController _setViewAppearState:isAnimating:] + 184
4 UIKitCore 0x187572984 -[UIViewController __viewWillAppear:] + 124
5 UIKitCore 0x187572a54 -[UIViewController viewWillAppear:] + 72
6 UIKitCore 0x18775d928 -[UINavigationController _startCustomTransition:] + 1088
7 UIKitCore 0x18775f160 -[UINavigationController _startDeferredTransitionIfNeeded:] + 632
8 UIKitCore 0x18775f948 -[UINavigationController __viewWillLayoutSubviews] + 56
9 UIKitCore 0x187760060 -[UINavigationController viewWillLayoutSubviews] + 40
10 UIKitCore 0x187575598 -[UIViewController _setViewAppearState:isAnimating:] + 936
11 UIKitCore 0x187575a90 -[UIViewController __viewDidAppear:] + 120
12 UIKitCore 0x187575b5c -[UIViewController viewDidAppear:] + 84
13 UIKitCore 0x187760278 -[UINavigationController _startCustomTransition:] + 1416
14 UIKitCore 0x187761a94 -[UINavigationController _startDeferredTransitionIfNeeded:] + 632
15 UIKitCore 0x187762360 -[UINavigationController __viewWillLayoutSubviews] + 56
16 UIKitCore 0x187762a78 -[UINavigationController viewWillLayoutSubviews] + 40
17 UIKitCore 0x187575598 -[UIViewController _setViewAppearState:isAnimating:] + 936
18 UIKitCore 0x187575a90 -[UIViewController __viewDidAppear:] + 120
19 UIKitCore 0x187575b5c -[UIViewController viewDidAppear:] + 84
20 UIKitCore 0x18769d040 -[UITabBarController transitionFromViewController:toViewController:transition:shouldSetSelected:] + 1000
21 UIKitCore 0x18769df78 -[UITabBarController _setSelectedViewController:] + 480
22 UIKitCore 0x18769e374 -[UITabBarController setSelectedViewController:] + 56
23 UIKitCore 0x1876a2438 -[UITabBarController _tabBarItemClicked:] + 424
24 UIKitCore 0x1876a2590 -[UITabBar _sendAction:withEvent:] + 388
25 UIKitCore 0x1876a26a0 -[UITabBar _sendAction:withEvent:] + 508
26 UIKitCore 0x187696c60 -[UIControl sendAction:to:forEvent:] + 96
27 UIKitCore 0x187696d30 -[UIControl _sendActionsForEvents:withEvent:] + 412
28 UIKitCore 0x187696e50 -[UIControl touchesEnded:withEvent:] + 524
29 UIKitCore 0x187660750 -[UIWindow _sendTouchesForEvent:] + 1220
30 UIKitCore 0x187661568 -[UIWindow sendEvent:] + 3400
31 UIKitCore 0x1876387d4 -[UIApplication sendEvent:] + 416
32 UIKitCore 0x1876398d0 __dispatchPreprocessedEventFromEventQueue + 3280
33 UIKitCore 0x1876399d8 __handleEventQueueInternal + 5928
34 UIKitCore 0x187639280 __handleHIDEventFetcherDrain + 116
35 CoreFoundation 0x183075624 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 20
36 CoreFoundation 0x183074f78 __CFRunLoopDoSource0 + 164
37 CoreFoundation 0x183074964 __CFRunLoopDoSources0 + 244
38 CoreFoundation 0x1830716a8 __CFRunLoopRun + 1044
39 CoreFoundation 0x182f9f808 CFRunLoopRunSpecific + 460
40 GraphicsServices 0x184d80198 GSEventRunModal + 160
41 UIKitCore 0x187641148 UIApplicationMain + 1936
42 MyApp 0x000f0590 main + 72
43 libdyld.dylib 0x182b8a8e0 start + 4
其中0x000f26a8
这样的地址对于快速定位问题毫无帮助。通过符号化崩溃日志,我们可以将这些地址转换为对应的函数名、类名以及代码行号,大大提高了定位崩溃原因的效率。例如,符号化后上述调用栈片段可能会变为:
Thread 0 Crashed:
0 MyApp -[ViewController viewDidLoad] (ViewController.m:42)
1 UIKitCore -[UIViewController loadViewIfRequired] (UIViewController.m:1245)
2 UIKitCore -[UIViewController view] (UIViewController.m:1270)
3 UIKitCore -[UIViewController _setViewAppearState:isAnimating:] (UIViewController.m:2345)
4 UIKitCore -[UIViewController __viewWillAppear:] (UIViewController.m:2400)
5 UIKitCore -[UIViewController viewWillAppear:] (UIViewController.m:2420)
6 UIKitCore -[UINavigationController _startCustomTransition:] (UINavigationController.m:3456)
7 UIKitCore -[UINavigationController _startDeferredTransitionIfNeeded:] (UINavigationController.m:3650)
8 UIKitCore -[UINavigationController __viewWillLayoutSubviews] (UINavigationController.m:3750)
9 UIKitCore -[UINavigationController viewWillLayoutSubviews] (UINavigationController.m:3770)
10 UIKitCore -[UIViewController _setViewAppearState:isAnimating:] (UIViewController.m:2750)
11 UIKitCore -[UIViewController __viewDidAppear:] (UIViewController.m:2800)
12 UIKitCore -[UIViewController viewDidAppear:] (UIViewController.m:2820)
13 UIKitCore -[UINavigationController _startCustomTransition:] (UINavigationController.m:3900)
14 UIKitCore -[UINavigationController _startDeferredTransitionIfNeeded:] (UINavigationController.m:4050)
15 UIKitCore -[UINavigationController __viewWillLayoutSubviews] (UINavigationController.m:4150)
16 UIKitCore -[UINavigationController viewWillLayoutSubviews] (UINavigationController.m:4170)
17 UIKitCore -[UIViewController _setViewAppearState:isAnimating:] (UIViewController.m:2750)
18 UIKitCore -[UIViewController __viewDidAppear:] (UIViewController.m:2800)
19 UIKitCore -[UIViewController viewDidAppear:] (UIViewController.m:2820)
20 UIKitCore -[UITabBarController transitionFromViewController:toViewController:transition:shouldSetSelected:] (UITabBarController.m:5678)
21 UIKitCore -[UITabBarController _setSelectedViewController:] (UITabBarController.m:5800)
22 UIKitCore -[UITabBarController setSelectedViewController:] (UITabBarController.m:5820)
23 UIKitCore -[UITabBarController _tabBarItemClicked:] (UITabBarController.m:6000)
24 UIKitCore -[UITabBar _sendAction:withEvent:] (UITabBar.m:1234)
25 UIKitCore -[UITabBar _sendAction:withEvent:] (UITabBar.m:1260)
26 UIKitCore -[UIControl sendAction:to:forEvent:] (UIControl.m:1345)
27 UIKitCore -[UIControl _sendActionsForEvents:withEvent:] (UIControl.m:1380)
28 UIKitCore -[UIControl touchesEnded:withEvent:] (UIControl.m:1420)
29 UIKitCore -[UIWindow _sendTouchesForEvent:] (UIWindow.m:3456)
30 UIKitCore -[UIWindow sendEvent:] (UIWindow.m:3700)
31 UIKitCore -[UIApplication sendEvent:] (UIApplication.m:4567)
32 UIKitCore __dispatchPreprocessedEventFromEventQueue (UIApplication.m:5678)
33 UIKitCore __handleEventQueueInternal (UIApplication.m:5800)
34 UIKitCore __handleHIDEventFetcherDrain (UIApplication.m:5700)
35 CoreFoundation __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ (CFRunLoop.c:123)
36 CoreFoundation __CFRunLoopDoSource0 (CFRunLoop.c:150)
37 CoreFoundation __CFRunLoopDoSources0 (CFRunLoop.c:200)
38 CoreFoundation __CFRunLoopRun (CFRunLoop.c:350)
39 CoreFoundation CFRunLoopRunSpecific (CFRunLoop.c:450)
40 GraphicsServices GSEventRunModal (GSEvent.c:100)
41 UIKitCore UIApplicationMain (UIApplication.m:6789)
42 MyApp main (main.m:15)
43 libdyld.dylib start (dyld.c:23)
这样我们就能很直观地看到崩溃发生在ViewController
的viewDidLoad
方法的第42行,极大地便利了问题的排查。
二、符号化崩溃日志的原理
在Objective - C中,符号化崩溃日志的核心原理基于DWARF(Debug With Arbitrary Record Format)调试信息格式。当我们使用Xcode编译项目时,编译器会将调试信息(包括函数名、变量名、代码行号等)以DWARF格式存储在可执行文件和对应的dSYM(Debug Symbol)文件中。
可执行文件包含了应用程序运行所需的机器码,而dSYM文件则是一个包含符号表的文件,它将内存地址映射到符号(函数名、类名等)。当应用程序崩溃时,系统生成的崩溃日志记录了当时的调用栈信息,这些信息以内存地址的形式存在。符号化过程就是通过将崩溃日志中的地址与dSYM文件中的符号表进行匹配,从而将地址转换为有意义的符号信息。
例如,假设我们有一个简单的Objective - C类MyClass
:
#import <Foundation/Foundation.h>
@interface MyClass : NSObject
- (void)myMethod;
@end
@implementation MyClass
- (void)myMethod {
// 假设这里有一行代码导致崩溃
NSString *str = nil;
NSLog(@"%@", str.length);
}
@end
当这个方法导致崩溃时,崩溃日志中会记录myMethod
方法的调用地址。在编译时,Xcode会生成对应的dSYM文件,其中包含了myMethod
方法的地址范围以及其他调试信息。符号化工具(如atos
命令)会读取崩溃日志中的地址,并在dSYM文件中查找匹配的符号,从而将地址转换为-[MyClass myMethod]
以及对应的代码行号。
三、获取崩溃日志
-
设备上获取 在iOS设备上,当应用崩溃后,可以通过以下步骤获取崩溃日志:
- 连接设备到Mac电脑,打开Xcode。
- 在Xcode的菜单栏中,选择
Window
->Devices and Simulators
。 - 在
Devices
列表中,选择发生崩溃的设备。 - 点击
View Device Logs
,在弹出的窗口中找到对应的崩溃日志。
-
从TestFlight获取 如果应用是通过TestFlight进行分发测试,当用户反馈崩溃时,可以从TestFlight获取崩溃日志:
- 登录到App Store Connect。
- 选择对应的应用,点击
TestFlight
。 - 在左侧边栏中选择
Crashes
,这里可以找到TestFlight用户反馈的崩溃日志。
-
第三方崩溃监测工具 除了上述官方渠道,还可以使用第三方崩溃监测工具,如Crashlytics、Bugly等。这些工具通常会在应用崩溃时自动收集崩溃日志,并上传到其服务器。开发者可以登录对应的控制台查看详细的崩溃信息,并且这些工具一般都提供了符号化功能。
四、符号化崩溃日志的工具和方法
-
使用Xcode自动符号化 Xcode自带了符号化崩溃日志的功能。当在
Devices and Simulators
中查看崩溃日志时,Xcode会尝试自动符号化日志。前提是Xcode能够找到对应的dSYM文件。dSYM文件通常与应用的构建产物存放在一起,如果是通过Xcode直接运行应用并发生崩溃,Xcode一般能够自动关联到dSYM文件并完成符号化。 -
atos命令手动符号化
atos
是Xcode提供的一个命令行工具,用于将内存地址转换为符号信息。使用atos
需要以下几个步骤:- 获取应用的可执行文件路径:可以在Xcode的
Build Settings
中找到EXECUTABLE_PATH
,它指定了应用的可执行文件路径。例如,在模拟器上运行的应用,可执行文件路径可能类似于/Users/yourusername/Library/Developer/CoreSimulator/Devices/[device - id]/data/Containers/Bundle/Application/[app - id]/MyApp.app/MyApp
。 - 获取dSYM文件路径:dSYM文件通常位于构建产物的
DerivedData
目录下。例如,/Users/yourusername/Library/Developer/Xcode/DerivedData/MyApp - [random - string]/Build/Products/Debug - iphoneos/MyApp.app.dSYM
。 - 执行atos命令:命令格式如下:
- 获取应用的可执行文件路径:可以在Xcode的
atos -arch [architecture] -o [executable - path] -l [load - address] [crash - address]
其中[architecture]
是应用的架构,如arm64
(用于真机)或x86_64
(用于模拟器);[executable - path]
是应用的可执行文件路径;[load - address]
是崩溃日志中记录的加载地址(一般在崩溃日志的开头部分可以找到);[crash - address]
是崩溃日志中具体的崩溃地址。
例如,假设崩溃日志中有如下信息:
Dyld Error Message:
Library not loaded: @rpath/MyFramework.framework/MyFramework
Referenced from: /Users/yourusername/Library/Developer/CoreSimulator/Devices/[device - id]/data/Containers/Bundle/Application/[app - id]/MyApp.app/MyApp
Reason: image not found
Binary Images:
0x102d1c000 - 0x102d3bfff MyApp arm64 <1234567890abcdef1234567890abcdef> /Users/yourusername/Library/Developer/CoreSimulator/Devices/[device - id]/data/Containers/Bundle/Application/[app - id]/MyApp.app/MyApp
0x102f92000 - 0x102f97fff libswiftCore.dylib arm64 <abcdef1234567890abcdef1234567890> /usr/lib/swift/libswiftCore.dylib
...
Thread 0 Crashed:
0 MyApp 0x000f26a8 -[ViewController viewDidLoad] + 144
要符号化0x000f26a8
这个地址,可以执行以下命令:
atos -arch x86_64 -o /Users/yourusername/Library/Developer/CoreSimulator/Devices/[device - id]/data/Containers/Bundle/Application/[app - id]/MyApp.app/MyApp -l 0x102d1c000 0x000f26a8
执行后,atos
会输出符号化后的信息,如-[ViewController viewDidLoad] (ViewController.m:42)
。
- 使用第三方工具符号化 一些第三方工具如Symbolicatecrash也可以用于符号化崩溃日志。使用Symbolicatecrash时,需要先下载该工具脚本,然后执行以下命令:
./symbolicatecrash [crash - log - file] [dSYM - file] > symbolicated - crash - log.txt
其中[crash - log - file]
是崩溃日志文件路径,[dSYM - file]
是dSYM文件路径。执行后,会将符号化后的日志输出到symbolicated - crash - log.txt
文件中。
五、符号化过程中常见问题及解决方法
-
找不到dSYM文件
- 原因:可能是dSYM文件丢失、路径错误或者没有与应用构建版本对应。
- 解决方法:首先确认dSYM文件是否存在于正确的位置。如果是通过Xcode构建,检查
DerivedData
目录。如果是通过CI/CD流程构建,确保dSYM文件被正确保存和传递。对于不同版本的应用,要确保使用与之对应的dSYM文件。可以通过比较dSYM文件的UUID(在dSYM文件的Contents/Info.plist
中可以找到CFBundleIdentifier
对应的UUID)与崩溃日志中的UUID(在崩溃日志开头部分)来确认匹配。
-
符号化不准确
- 原因:可能是编译器优化设置导致调试信息不完整,或者应用中使用了动态库且动态库的符号信息未正确处理。
- 解决方法:在
Build Settings
中,将Optimization Level
设置为None [-O0]
,这样可以保留完整的调试信息。对于动态库,确保动态库的构建也保留了调试信息,并且在符号化时提供了动态库对应的dSYM文件。
-
多架构问题
- 原因:如果应用支持多个架构(如同时支持
arm64
和x86_64
),在符号化时可能会因为选择错误的架构而导致符号化失败。 - 解决方法:仔细查看崩溃日志,确定崩溃发生的设备是真机(一般为
arm64
架构)还是模拟器(x86_64
架构),然后在使用atos
等工具时指定正确的架构。
- 原因:如果应用支持多个架构(如同时支持
六、符号化崩溃日志分析实战案例
假设我们有一个简单的Objective - C应用,用于展示图片。应用包含一个ViewController
,在viewDidLoad
方法中加载一张图片并显示。
#import "ViewController.h"
@interfaceViewController ()
@end
@implementationViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIImage *image = [UIImage imageNamed:@"nonexistent_image.png"];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
[self.view addSubview:imageView];
}
@end
当运行这个应用时,由于图片nonexistent_image.png
不存在,应用会崩溃。我们获取到崩溃日志如下:
Process: MyApp [1234]
Path: /Users/yourusername/Library/Developer/CoreSimulator/Devices/[device - id]/data/Containers/Bundle/Application/[app - id]/MyApp.app/MyApp
Identifier: com.example.MyApp
Version: 1.0 (1)
Code Type: X86 - 64 (Native)
Parent Process: launchd_sim [456]
Responsible: MyApp [1234]
User ID: 501
Date/Time: 2024 - 01 - 01 12:34:56.789 - 0700
OS Version: macOS 13.0 (22A380)
Report Version: 12
Bridge OS Version: 7.0 (20P5086)
Anonymous UUID: ABCDEF12 - 3456 - 7890 - ABCD - EF1234567890
Sleep/Wake UUID: GHIJKL12 - 3456 - 7890 - GHIJ - KL1234567890
Time Awake Since Boot: 3600 seconds
Time Since Wake: 1800 seconds
System Integrity Protection: enabled
Crashed Thread: 0
Exception Type: NSException
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: +[UIImage imageNamed:]: image named 'nonexistent_image.png' not found
Termination Reason: Namespace NSInternalInconsistencyException, Code 0
Application Specific Information:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '+[UIImage imageNamed:]: image named 'nonexistent_image.png' not found'
abort() called
Thread 0 Crashed:
0 CoreFoundation 0x183075624 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 20
1 CoreFoundation 0x183074f78 __CFRunLoopDoSource0 + 164
2 CoreFoundation 0x183074964 __CFRunLoopDoSources0 + 244
3 CoreFoundation 0x1830716a8 __CFRunLoopRun + 1044
4 CoreFoundation 0x182f9f808 CFRunLoopRunSpecific + 460
5 GraphicsServices 0x184d80198 GSEventRunModal + 160
6 UIKitCore 0x187641148 UIApplicationMain + 1936
7 MyApp 0x000f0590 main + 72
8 libdyld.dylib 0x182b8a8e0 start + 4
- 使用Xcode自动符号化
将设备连接到Xcode,在
Devices and Simulators
中找到崩溃日志,Xcode会自动符号化。符号化后的日志如下:
Thread 0 Crashed:
0 CoreFoundation __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ (CFRunLoop.c:123)
1 CoreFoundation __CFRunLoopDoSource0 (CFRunLoop.c:150)
2 CoreFoundation __CFRunLoopDoSources0 (CFRunLoop.c:200)
3 CoreFoundation __CFRunLoopRun (CFRunLoop.c:350)
4 CoreFoundation CFRunLoopRunSpecific (CFRunLoop.c:450)
5 GraphicsServices GSEventRunModal (GSEvent.c:100)
6 UIKitCore UIApplicationMain (UIApplication.m:6789)
7 MyApp main (main.m:15)
8 libdyld.dylib start (dyld.c:23)
我们可以看到,虽然部分系统库的符号化成功了,但应用内的main
函数的符号化并不完整,没有显示具体的代码行号。这是因为Xcode可能没有正确关联到dSYM文件中的详细信息。
- 使用atos命令符号化
首先获取应用的可执行文件路径
/Users/yourusername/Library/Developer/CoreSimulator/Devices/[device - id]/data/Containers/Bundle/Application/[app - id]/MyApp.app/MyApp
,以及dSYM文件路径/Users/yourusername/Library/Developer/Xcode/DerivedData/MyApp - [random - string]/Build/Products/Debug - iphoneos/MyApp.app.dSYM
。假设崩溃日志中的加载地址为0x102d1c000
,崩溃地址为0x000f0590
。执行以下命令:
atos -arch x86_64 -o /Users/yourusername/Library/Developer/CoreSimulator/Devices/[device - id]/data/Containers/Bundle/Application/[app - id]/MyApp.app/MyApp -l 0x102d1c000 0x000f0590
得到符号化结果:main (main.m:15)
,这样就准确地定位到了main
函数的具体代码行。
通过进一步分析符号化后的崩溃日志,我们可以发现崩溃原因是在ViewController
的viewDidLoad
方法中尝试加载不存在的图片。这就为我们修复问题提供了明确的方向,即检查图片资源是否存在,或者修改加载图片的逻辑。
通过这个实战案例,我们可以看到符号化崩溃日志在Objective - C开发中对于快速定位和解决问题的重要性。掌握符号化的方法和工具,以及分析符号化后的日志,是每个Objective - C开发者必备的技能。在实际开发中,遇到复杂的崩溃问题时,结合符号化日志、断点调试等手段,能够更高效地解决问题,提升应用的稳定性和质量。