MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中的符号化崩溃日志分析实战

2022-12-143.8k 阅读

一、符号化崩溃日志的重要性

在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)

这样我们就能很直观地看到崩溃发生在ViewControllerviewDidLoad方法的第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]以及对应的代码行号。

三、获取崩溃日志

  1. 设备上获取 在iOS设备上,当应用崩溃后,可以通过以下步骤获取崩溃日志:

    • 连接设备到Mac电脑,打开Xcode。
    • 在Xcode的菜单栏中,选择Window -> Devices and Simulators
    • Devices列表中,选择发生崩溃的设备。
    • 点击View Device Logs,在弹出的窗口中找到对应的崩溃日志。
  2. 从TestFlight获取 如果应用是通过TestFlight进行分发测试,当用户反馈崩溃时,可以从TestFlight获取崩溃日志:

    • 登录到App Store Connect。
    • 选择对应的应用,点击TestFlight
    • 在左侧边栏中选择Crashes,这里可以找到TestFlight用户反馈的崩溃日志。
  3. 第三方崩溃监测工具 除了上述官方渠道,还可以使用第三方崩溃监测工具,如Crashlytics、Bugly等。这些工具通常会在应用崩溃时自动收集崩溃日志,并上传到其服务器。开发者可以登录对应的控制台查看详细的崩溃信息,并且这些工具一般都提供了符号化功能。

四、符号化崩溃日志的工具和方法

  1. 使用Xcode自动符号化 Xcode自带了符号化崩溃日志的功能。当在Devices and Simulators中查看崩溃日志时,Xcode会尝试自动符号化日志。前提是Xcode能够找到对应的dSYM文件。dSYM文件通常与应用的构建产物存放在一起,如果是通过Xcode直接运行应用并发生崩溃,Xcode一般能够自动关联到dSYM文件并完成符号化。

  2. 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命令:命令格式如下:
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)

  1. 使用第三方工具符号化 一些第三方工具如Symbolicatecrash也可以用于符号化崩溃日志。使用Symbolicatecrash时,需要先下载该工具脚本,然后执行以下命令:
./symbolicatecrash [crash - log - file] [dSYM - file] > symbolicated - crash - log.txt

其中[crash - log - file]是崩溃日志文件路径,[dSYM - file]是dSYM文件路径。执行后,会将符号化后的日志输出到symbolicated - crash - log.txt文件中。

五、符号化过程中常见问题及解决方法

  1. 找不到dSYM文件

    • 原因:可能是dSYM文件丢失、路径错误或者没有与应用构建版本对应。
    • 解决方法:首先确认dSYM文件是否存在于正确的位置。如果是通过Xcode构建,检查DerivedData目录。如果是通过CI/CD流程构建,确保dSYM文件被正确保存和传递。对于不同版本的应用,要确保使用与之对应的dSYM文件。可以通过比较dSYM文件的UUID(在dSYM文件的Contents/Info.plist中可以找到CFBundleIdentifier对应的UUID)与崩溃日志中的UUID(在崩溃日志开头部分)来确认匹配。
  2. 符号化不准确

    • 原因:可能是编译器优化设置导致调试信息不完整,或者应用中使用了动态库且动态库的符号信息未正确处理。
    • 解决方法:在Build Settings中,将Optimization Level设置为None [-O0],这样可以保留完整的调试信息。对于动态库,确保动态库的构建也保留了调试信息,并且在符号化时提供了动态库对应的dSYM文件。
  3. 多架构问题

    • 原因:如果应用支持多个架构(如同时支持arm64x86_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
  1. 使用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文件中的详细信息。

  1. 使用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函数的具体代码行。

通过进一步分析符号化后的崩溃日志,我们可以发现崩溃原因是在ViewControllerviewDidLoad方法中尝试加载不存在的图片。这就为我们修复问题提供了明确的方向,即检查图片资源是否存在,或者修改加载图片的逻辑。

通过这个实战案例,我们可以看到符号化崩溃日志在Objective - C开发中对于快速定位和解决问题的重要性。掌握符号化的方法和工具,以及分析符号化后的日志,是每个Objective - C开发者必备的技能。在实际开发中,遇到复杂的崩溃问题时,结合符号化日志、断点调试等手段,能够更高效地解决问题,提升应用的稳定性和质量。