iOS线上崩溃追踪

极地冰原(加载图).jpg

目录

一、崩溃收集介绍
二、第三方库收集崩溃信息
三、原生收集崩溃信息
四、崩溃信息符号化
五、崩溃中断拦截

一、崩溃收集介绍

  App线上崩溃一直都是比较棘手的问题,尽管我们努力在开发与测试过程中检测与避免崩溃代码,但依然会在不同系统,不同网络环境与不同的使用方式中出现特殊情况。

  面对崩溃情况的最好解决方式就是对崩溃的程序进行现场调试,显然这很难做到。所以我们只能收集App崩溃时产生的信息,通过对崩溃信息的解析来找到产生崩溃的原因,之后修复崩溃的代码并发布新的版本。

  市面上对于崩溃信息收集有很多种方案,比如第三方的Bugly友盟KSCrashplcrashreporter等,苹果自带的崩溃收集有crash文件crashes

  本次会简单介绍并对比不同方案的优劣以及其他关于崩溃收集的知识。

二、第三方库收集崩溃信息

  第三方库主要介绍两种类型,第一种是腾讯的Bugly友盟的APM。这两种都是将崩溃信息收集到自己的平台进行统计与符号化。第二种是KSCrash,这是一个开源的第三方库,它可以通过邮件或上传的方式将收集到的崩溃信息发送到内部服务器,从而保证信息的安全性。

  首先是Bugly友盟的对比:
  8月1日更新,友盟免费版现只能查看当天的崩溃记录,其他时间的崩溃记录需要开会员才可以。

类别 Bugly 友盟(免费版) 区别
最近更新时间 符号表工具更新时间2021-6
sdk更新时间为2021-4
sdk更新时间为2022-6
集成方式 pod / sdk pod / sdk
友盟需要桥接并且导入五个文件
启动方式 Bugly.start() UMConfigure.initWithAppkey()
上传dSYM方式 通过命令上传 在友盟网页上传 Bugly需要jdk1.8环境才可以上传
崩溃刷新时间 2分钟左右 1-10分钟甚至更久
崩溃解析内容 1.显示崩溃原因
2.显示待还原的堆栈名称
3.显示页面操作流程
1.显示还原后的堆栈名称
2.显示更详细的手机信息

友盟崩溃原因只能手动重现
错误分类 相同的崩溃不会出现多条数据,只会增加次数 相同的崩溃会出现多条数据

  通过两者的对比发现,Bugly更擅长定位错误原因,友盟更擅长定位错误代码位置。

  我个人更推荐Bugly,因为对于崩溃信息Bugly可以很清晰的展示出错误原因,例如数组越界,空指针等。而友盟只能展示崩溃的方法名,如果代码过于复杂的话还需要多次调试才可以找到具体的崩溃原因。

  Bugly中想要还原堆栈名称可以在终端使用命令:

xcrun swift-demangle [待还原的堆栈名称]

# 例如:
# 未还原的堆栈名称:
0x0000000100a27a04 
$s9ErrorDemo14ViewControllerC05tableC0_14didSelectRowAtySo07UITableC0C_10Foundation9IndexPathVtFTf4dnn_n

# 还原命令(需要去掉开头的$字符)
xcrun swift-demangle s9ErrorDemo14ViewControllerC05tableC0_14didSelectRowAtySo07UITableC0C_10Foundation9IndexPathVtFTf4dnn_n

# 还原后的堆栈名称:
ErrorDemo.ViewController.tableView(_: __C.UITableView, didSelectRowAt: Foundation.IndexPath)

# 通过还原后的堆栈名称可以看出项目名称,文件名称以及崩溃的方法名称。 

  不同于友盟直接上传dSYM的方式,Bugly上传dSYM文件需要使用以下命令:

# 0.首先安装 jdk1.8 环境(超过1.8的环境会出现无法打开jar文件的问题)
# 1.然后进入到从 Bugly 官网下载的包含 buglyqq-upload-symbol.jar 文件的文件夹内
# 2.在终端执行上传命令
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.当上传状态为200即代表上传成功

  接下来是KSCrash与上面两种的对比:

  首先是它们的记录方式不同Bugly友盟可以通过手机信息,崩溃信息,崩溃类型等进行一个统计与归类,方便开发人员进行查阅。而KSCrash由于没有自己的服务器,若需要统计归类的话都需要开发人员自行完成。

  第二是它们的符号化方式不同Bugly友盟必须要开发人员上传dSYM文件才可以进行正常符号化。而KSCrash支持在设备上符号化,也就是说App在收集到崩溃信息后可以在手机上进行符号化,然后将符号化后的崩溃信息存储在手机中,上传时直接上传符号化后的崩溃信息。

  第三是它们的安全性不同Bugly友盟只能将崩溃信息上传到它们的服务器中,而KSCrash可以通过邮件或上传的方式发送到本地的邮箱或服务器中,一定程度上保障了信息的安全性。

  最后是它们的代码权限不同Bugly友盟都是闭源的,只能使用其提供的方法。而KSCrash由于是开源项目,可以对其功能进行修改与扩展,也可以通过阅读源码的方式了解其底层的逻辑。

  对于崩溃日志的内容,KSCrash所收集的崩溃日志与系统自带分析中的崩溃日志几乎相同,并且本地默认以JSON字符串的方式对日志内容进行存储。

  总结:

  本次只对常规的属性进行了简单的崩溃测试,对于更复杂更详细的功能与检测方式还需要根据实际情况自行查看文档与测试。
  个人认为对于常规的应用使用Bugly友盟足够,需要更丰富的功能或注重安全性的应用可以考虑KSCrash等类似的第三方库。如果觉得这些还不够用的话也可以考虑使用其他功能更丰富的收费平台。

三、原生收集崩溃信息

  原生收集崩溃信息的方式我总结了三种,分别为:系统crash文件crashes工具代码监听

  1.使用系统crash文件收集:

  这种收集方式主要面向B端产品。在用户发生程序崩溃后,产品支持的同学可以通过设置 -> 隐私 -> 分析与改进,在分析数据的列表中找到对应的APP名称并打开,然后点击右上角的分享按钮将错误报告通过微信,邮件或其他方式发送给开发人员。开发人员拿到该文件后便可以通过符号化的方式找到发生崩溃的具体位置并修复该问题。

  这种方式除了收集不方便外还有一个问题,就是系统只会保存第一次出现的崩溃日志,后续如果因为相同的原因崩溃,系统不会创建新的日志,会出现在寻找崩溃日志时无法找到对应日期的问题。

  注意:崩溃文件有可能是.ips后缀,也有可能是.crash后缀,实际上通过直接修改后缀名的方式可以进行转换。如果崩溃内容是JSON格式可以将后缀名改为.ips,看起来会更直观。

  2.使用crashes工具收集:

  crashes工具是Xcode自带的崩溃分析工具。使用该工具收集崩溃信息需要用户手机开启共享iphone分析功能,具体操作流程为设置 -> 隐私 -> 分析与改进,开启共享iphone分析,并开启与App开发者共享选项。之后应用发生崩溃后,崩溃信息会自动发送到Apple的服务器上,当你进入crashes工具时会自动下载崩溃信息。缺点是崩溃信息上传不及时,会有几天的延迟。

  你可以通过Xcode中的Window -> Organizer 进入打包页面,在左上角选择需要查看的App,点击左侧crashes选项即可显示崩溃列表。

crashes崩溃列表

  崩溃列表上方为分类选项,可以选择崩溃时间,版本号,build版本等。点击一个崩溃项可以看到具体的崩溃线程以及堆栈信息。右侧Crash Log Details中展示了本次崩溃的项目名称,build版本号,手机系统,手机型号等信息。下方Statistics显示了因为该原因崩溃的设备数量,开发人员可通过崩溃设备数量来制定修复优先级。

  如果你在发布App时上传了dSYM文件,当你使用crashes工具时就会自动符号化崩溃信息,并且可以通过符号化后的信息快速定位到崩溃代码,我们可以通过点击崩溃堆栈右侧的小箭头或Open in Project...并在弹出的选项中选择对应的项目名称即可定位到具体的错误代码。【还需要验证】

crashes定位崩溃代码

  有时崩溃定位不准确或崩溃信息未符号化,我们可以通过崩溃日志手动进行符号化。只需要在崩溃堆栈处右键选择Show in Finder即可定位到crash崩溃文件。具体符号化崩溃信息的方式在下一节。

  3.使用代码监听收集:

  使用代码监听是通过代码注册异常回调。当应用程序发生异常时优先将异常信息保存在本地,并在适当的时机上传给开发人员。该方式需要先将开发模式改为Release模式,并且在Edit Scheme中关闭Debug executable选项。否则在应用发生崩溃时会优先被Xcode拦截,不会走监听方法。

  在OC项目中可以使用NSSetUncaughtExceptionHandler()方法进行异常监听,例如当程序出现崩溃时该方法会在Block中传入NSException参数,通过打印可以发现NSException中包含了崩溃时的错误堆栈信息,以及崩溃原因等数据。

  在Swift项目中,需要使用objc_setUncaughtExceptionHandler()来替代NSSetUncaughtExceptionHandler()方法,并且该方法只有在OC代码产生异常或崩溃时才会调用。而Swift代码异常需要使用signal()监听注册异常信号,该方法也会同时监听OC代码的崩溃与异常。


// 崩溃代码
+ (void) testArrayError {
   NSArray *ary = [NSArray array];
   ary[10];
}

// 监测代码
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()可以注册多种信号,具体的信号列表可以通过Darwin.sys.signal文件进行查看,其中比较常用的信号为SIGTRAP,在常规造成的异常或崩溃中一般都会触发SIGTRAP信号。

  signal()方法需要传入两个参数,第一个参数为信号标识,例如SIGTRAP这种。第二个参数为触发异常信号时的回调,由于是C语言闭包在使用时需要增加@convention(c)标识,之后在闭包中获取异常堆栈结构树并打印。

// 注册监听
signal(SIGTRAP, handleSignalException)

// 闭包监听
let handleSignalException: @convention(c) (Int32) -> Void = { value in
    let pointer = UnsafeMutablePointer.allocate(capacity: 128)

    // backtrace 两个方法需要桥接导入头文件 #import 
    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)

    // 在监听完成后需要取消监听,否则主线程崩溃的App会一直处于卡死状态,不会退出。
    signal(SIGTRAP, SIG_DFL)
}

// 打印内容
============================================================================
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

  从打印内容可以看出,在ErrorDemo项中也包含了未还原的堆栈名称,并且该内容与最开始Bugly中未还原的堆栈名称类似(因为都是在点击列表项时触发的崩溃)。由此可以得出Bugly也抓取到了类似的信息,只是暂时还无法得知Bugly是否使用该方式收集的异常信息,以及它是如何从该信息中解析出数组越界等崩溃的具体原因。

  除此之外,我们在从第三方库中可以看到崩溃信息的组成中包含了多个线程的堆栈信息。而我们使用的该方法目前只能打印出触发异常线程的堆栈信息,所以我认为还有其他方法能拿到其他线程的信息,在查阅其他文章后找到了最有可能的文件 -- Darwin.Mach.task

  从文件名可以看出,该文件属于Mach线程文件,其中有一个方法名为task_threads(),该方法的功能为获取所有的线程列表,正符合目前获取多个线程信息的需求。不过由于时间原因,对崩溃收集的调研只能先暂停在这里,该文件是对多线程崩溃信息收集很好的一个切入点,后边可以沿着这条线继续摸索下去。当然,阅读第三方库的源码会是更好的选择。

四、崩溃信息符号化

// 在我们收集的崩溃信息中,未符号化前的堆栈为十六进制码。 
// 例如:
0   ErrorDemo 0x0000000100be8234 0x100be0000 + 33332

  符号化的方式有三种:

  第一种为symbolicatecrash:该方式可以将完整的崩溃信息符号化,但是会出现自己项目的堆栈信息未符号化的情况,iOS14及以下系统可用。

  第二种为atos:该方式可以只将某一条堆栈单独符号化,但是只能符号化自己项目的堆栈,无法符号化系统堆栈,没有系统限制。

  第三种为CrashSymbolicator.py:可以将完整的崩溃信息全部符号化(包括系统堆栈与项目堆栈),并且得到的崩溃信息比symbolicatecrash更详细,iOS15及以上系统可用。

  注:三种方式都需要与之对应的dSYM文件,由于每次编译都会创建新的dSYM文件,所以一定要在编译或打包后将dSYM文件存好。并且不同系统的崩溃信息需要使用对应的方法进行符号化,否则可能会出现失败的情况。

  dSYM文件的获取方式:

  对于未打包的项目获取dSYM文件可以通过项目目录中的Products -> [项目名称].app -> Show in Finder即可找到[项目名称].app.DSYM文件。

  对于打包完成的项目通过对打包后的Archives项目Show in Finder -> 显示包内容 -> dSYMs即可找到[项目名称].app.DSYM文件。

  这里需要注意的是默认情况下只有Release模式才产生dSYM文件。如果想要Debug模式下也产生dSYM文件的话,需要在Build Settings -> Debug Information Format中将DWARF改为DWARF with dSYM File

  1.使用symbolicatecrash符号化(iOS14及以下系统可用):

  首先我们需要找到symbolicatecrash文件:

// 该文件一般在该路径下
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash

// 如果在上面路径找不到的话可以通过命令进行查找。
find /Applications/Xcode.app -name symbolicatecrash -type f

  在找到symbolicatecrash文件后,随意新建一个文件夹。将该文件copy一份与dSYM文件和crash文件同放在新建的文件夹内。

// 首先打开 .bash_profile 文件,添加环境变量
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"

// 保存并执行命令
source ~/.bash_profile

// 然后cd到新建的文件夹内
cd xxx

// 使用命令进行符号化
./symbolicatecrash [crash文件或ips文件] [dSYM文件] > [符号化后的新文件名称.crash]

// 例如
./symbolicatecrash ErrorTest.crash ErrorTest.app.dSYM > newCrash.crash

// 符号化后的部分信息
============================================================================
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

  从符号化后的堆栈信息中可以发现ErrorTest项目堆栈并未符号化,需要再次使用atos进行符号化。

  如果执行命令报错:

  错误一:

Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash line 75.

// 需要设置环境变量,并执行 source ~/.bash_profile

  错误二:

No crash report version in [文件名].crash at ./symbolicatecrash line 1371.

// 该崩溃信息可能需要使用 CrashSymbolicator.py 进行解析

  2.使用atos符号化(无系统限制):

# 例如
0   ErrorDemo                              0x1001f4660 0x1001ec000 + 34400
1   ErrorDemo                              0x1001f4504 0x1001ec000 + 34052
2   ErrorDemo                              0x1001f41c8 0x1001ec000 + 33224

# 注意这里的路径为dSYM文件显示包内容后的项目路径
atos -o /ErrorDemo.app.dSYM/Contents/Resources/DWARF/ErrorDemo -l 0x1001ec000 0x1001f4660

# 其中 0x1001ec000 为起始地址,0x1001f4660为偏移地址。注意顺序!!!

# 符号化后
specialized ViewController.tableView(_:didSelectRowAt:) (in ErrorDemo) (ViewController.swift:66)

  3.使用CrashSymbolicator.py符号化(iOS15及以上系统可用):

  该方式与symbolicatecrash方式类似,首先需要找到CrashSymbolicator.py文件:

# 该文件一般在该路径下
/Applications/Xcode.app/Contents/SharedFrameworks/CoreSymbolicationDT.framework/Versions/A/Resources/CrashSymbolicator.py

# 如果在上面路径找不到的话可以通过命令进行查找。
find /Applications/Xcode.app -name CrashSymbolicator -type f

  在找到该文件后不要将它copy到其他文件夹内,否则会出现找不到头文件的问题。

# 执行命令
python3 [CrashSymbolicator.py文件路径] -d [dSYM文件路径] -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

# 符号化后的部分信息
============================================================================
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 (:0) [inlined]
2   ErrorDemo                              0x1001f4660 specialized Array._checkSubscript(_:wasNativeTypeChecked:) + 0 (:0) [inlined]
3   ErrorDemo                              0x1001f4660 specialized Array.subscript.getter + 0 (: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 (:0)
7   ErrorDemo                              0x1001f41c8 ViewController.tableView(_:didSelectRowAt:) + 11 (: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 (:16) [inlined]
26  ErrorDemo                              0x1001f2c14 main + 107 (LoadAnimationView_01.swift:0)
27  dyld                                   0x1005b4190 start + 443

  从符号化后的堆栈信息可以看出,本次崩溃原因为Swift runtime failure: Index out of range,崩溃的位置为ViewController.swift文件。崩溃的操作为ViewController.tableView(_:didSelectRowAt:)

五、崩溃中断拦截

  崩溃中断拦截是在代码监听的基础上,在崩溃前进行某些操作,完成后再退出应用。有同学表示这种操作可能会导致APP崩溃后无法再次启动,目前虽无法证实这种说法,但还是不太建议使用,该部分属于扩展知识。

  实现中断拦截首先是注册signal()信号并实现它的闭包方法,当程序发生崩溃时会调用handleSignalException闭包。这时可以将错误堆栈打印到控制台,由于在代码监听部分演示过,这里就不再添加这部分代码了。

  这里我们在AppDelegate外边定义了一个全局变量appDelegate,因为handleSignalException是一个c转义闭包,我们没有办法在其中直接获取或使用局部变量,所以需要定义一个全局变量进行操作(也许还有其他方法可以实现,如果你找到了请告诉我)。

  之后我们在闭包中调用一个外部方法demo(),在该方法里我们做一些其他操作。


// 全局变量
var appDelegate: AppDelegate?

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    // 状态控制
    var state = true

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        appDelegate = self

        // 回调方法
        let handleSignalException: @convention(c) (Int32) -> Void = { value in
            appDelegate?.demo()
        }

        // 注册各种信号
        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
    }

    /// 取消监听
    /// 如果是主线程崩溃,只有执行了该方法应用程序才会退出,否则程序会一直处于卡死状态,会不断的回调 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
    }
}


  例如,我们希望在应用程序崩溃后直接将崩溃信息上传到我们的服务器,在上传完成后再退出应用。

  首先我们需要获取当前崩溃线程的Runloop以及它的所有Mode,然后发送网络请求,当请求完成后更改state的状态。在此期间强制Runloop运行防止程序退出,这时该线程会陷入死循环,卡在While的位置。一直等到state状态修改为false,才会执行unRegisterError()方法退出程序。

func demo() {
    // 1. 获取 Runloop 与 modes
    let runloop = CFRunLoopGetCurrent()
    let modes = CFRunLoopCopyAllModes(runloop) as? NSArray

    // 2. 运行网络请求

    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))
        }
        
        // 请求完成修改状态
        self.state = false
    }

    task.resume()


    // 3. 运行防止程序退出
    while (state) {
        for model in modes ?? [] {
            let current = model as? NSString ?? ""
            CFRunLoopRunInMode(.init(rawValue:current), 0.001, false)
        }
    }

    // 4. 取消监听,退出应用
    unRegisterError()
}

  这样我们就实现了在崩溃时进行网络请求的功能。不过由于当前程序已经崩溃,应用也会卡死造成用户操作无反应。这时过长时间的操作可能会使用户强制退出应用,导致我们的操作都无法完成。所以除了网络请求外,我们也可以再崩溃时给用户一个弹窗,告诉用户程序已经崩溃正在收集崩溃信息。或者我们也可以添加一个可以其他按钮,点击后退出也可以。

func demo() {
    // 1. 获取 Runloop 与 modes
    let runloop = CFRunLoopGetCurrent()
    let modes = CFRunLoopCopyAllModes(runloop) as? NSArray

    // 2. 运行网络请求

    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))
        }
        
        // 请求完成修改状态
        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 = "程序出现异常,正在收集错误信息,请不要退出"
    window?.addSubview(alertLabel)

    // 3. 运行防止程序退出
    // 长时间的while会导致 Cpu 发热
    while (state) {
        for model in modes ?? [] {
            let current = model as? NSString ?? ""
            CFRunLoopRunInMode(.init(rawValue:current), 0.001, false)
        }
    }

    // 4. 取消监听,退出应用
    unRegisterError()
}

  不过显示UI的方法并不是所有手机都可以,经过测试13 Pro 15.5(能测到的最好的手机了) 的真机会出现View显示不出来的情况,需要进入后台重新进入应用才会显示弹窗。但是相同系统型号的模拟器就可以,其他几个系统和不同型号的真机也都可以(13 Pro max 不清楚)。

  最开始猜测是程序崩溃后刷新率变为1导致UI不刷新。然后在页面上加了个持续旋转的动画,发现崩溃后动画还在转但是弹窗依然没有出现,证明并不是屏幕没有刷新。所以目前也还没有找到原因,感觉应该和刷新率有关。不过这也只是扩展知识,不建议大家再项目中做过于复杂的操作,程序崩溃时本身就处于一个不稳定的状态,在做其他操作可能会导致意想不到的Bug。

  以上就是iOS崩溃追踪的全部内容了,涉及的内容比较杂也并没有研究很深的技术,了解一下就可以了。本文出于学习与记录的目的,如有不严谨的地方还请提出修改。

如果觉得有用就点个赞吧,你的赞是我最大的动力

解释
① 符号化:符号化是指通过命令将dSYM文件与崩溃信息进行对比与解析,将十六进制码转换为可读的错误代码。
② dSYM:dSYM是App编译后的符号表文件,需要使用对应的dSYM文件才可以解析错误堆栈信息。
③ Mach:Mach是一个用于支持操作系统研究的操作系统内核,在iOS系统的内核中混合使用了该内核。

参考
iOS/OSX Crash:崩溃日志报告
iOS 崩溃符号化工具- iOS 15 CrashSymbolicator
iOS获取任意线程调用堆栈信息
iOS dSYM详解和分析crash,ips文件
iOS堆栈信息解析(函数地址与符号关联)
iOS Crash从捕获到符号化解析分析
iOS中线程Call Stack的捕获和解析(二)
iOS/OSX Crash:捕捉异常
iOS异常处理-signal信号

你可能感兴趣的:(iOS线上崩溃追踪)