写点什么

如何找到程序崩溃的 “凶手” ?

发布于: 19 小时前
如何找到程序崩溃的 “凶手” ?

1. 前言

在 iOS 应用程序开发过程中,我们难免会碰到因各种异常而导致应用程序崩溃的情况。

对于开发过程中遇到的崩溃,我们可以根据本地崩溃信息快速定位问题。但对于线上版本发生的一些崩溃情况,我们只能通过收集崩溃信息来分析具体的原因。虽然 Apple 提供了崩溃信息上报的功能,但是并非所有的用户都开启了该功能。因此,对于数据采集 SDK 来说,采集崩溃信息并上报是一项必不可少的功能。

下面针对神策分析 iOS SDK 崩溃采集模块进行解析,希望能够给大家提供一些参考。

2. 崩溃类型

采集应用程序的崩溃信息,主要分为以下两种场景:

  • NSException 异常;

  • Unix 信号异常。

设计崩溃采集方案之前,我们不妨先认识一下 NSException 和 Unix 信号。

2.1. NSException

NSException

 是 Foundation 框架提供的一个类。用于封装一些异常信息,在需要的时候向外抛出。封装的异常信息包括异常名称、异常原因、调用堆栈。

在 iOS 应用程序中,最常见的就是通过 @throw 抛出的异常,如图 2-1 所示:

图 2-1 异常处理流程(图片来源于 Apple 开发者文档

比如常见的数组越界访问异常:

运行程序会出现如下异常信息:

2.2. Unix 信号

在 iOS 系统自动采集的崩溃日志中,经常可以看到类似下面的日志:

其中,Exception Type 中的两个字段 EXC_BAD_ACCESS 和 SIGSEGV 分别指 Mach 异常和 Unix 信号。

那什么是 Mach 异常和 Unix 信号呢?

Mach 是 macOS 和 iOS 操作系统的微内核,Mach 异常是最底层的内核级异常。

Mach 异常会被转换成相应的 Unix 信号,并传递给出错的线程。上述 Exception Type 中的 EXC_BAD_ACCESS 是 Mach 层的异常,被转换成了 Unix 信号 SIGSEGV,然后传递给出错的线程。之所以会将 Mach 异常转换成 Unix 信号,是为了兼容 POSIX 标准(SUS 规范),这样一来,开发者即使不了解 Mach 内核也可以通过 Unix 信号的方式进行兼容开发。

Unix 信号的种类有很多,在 iOS 应用程序中,常见的 Unix 信号有如下几种:

  • SIGILL:程序非法指令信号,通常是因为可执行文件本身出现错误,或者试图执行数据段。堆栈溢出时也有可能产生该信号;

  • SIGABRT:程序中止命令中止信号,调用 abort 函数时产生该信号;

  • SIGBUS:程序内存字节地址未对齐中止信号,比如访问一个 4 字节长的整数,但其地址不是 4 的倍数;

  • SIGFPE:程序浮点异常信号,通常在浮点运算错误、溢出及除数为 0 等算术错误时都会产生该信号;

  • SIGKILL:程序结束接收中止信号,用来立即结束程序运行,不能被处理、阻塞和忽略;

  • SIGSEGV:程序无效内存中止信号,即试图访问未分配的内存,或向没有写权限的内存地址写数据;

  • SIGPIPE:程序管道破裂信号,通常是在进程间通信时产生该信号;

  • SIGSTOP:程序进程中止信号,与 SIGKILL 一样不能被处理、阻塞和忽略。

神策分析 iOS SDK 针对 NSException 异常和 Unix 信号异常设计并实现了一套适用于数据分析的崩溃采集方案。

3. NSException 异常采集

3.1. 方案简介

NSException 类中定义的 NSSetUncaughtExceptionHandler 可以设置全局异常处理函数。因此,我们可以先通过 NSSetUncaughtExceptionHandler 设置的函数来处理异常,然后收集异常堆栈信息并触发相应的事件($AppCrashed),来实现 NSException 异常的埋点。

NSSetUncaughtExceptionHandler 函数接收一个 C 语言函数的指针,函数定义如下:

3.2. 具体实现

  1. 设计采集 $AppCrashed 事件的方法,将堆栈信息记录到事件属性 app_crashed_reason 中:

  1. 创建 SensorsAnalyticsExceptionHandler 类并新增 + sharedHandler 方法:

  1. 实现 SensorsAnalyticsExceptionHandler 类的初始化方法 - init,设置全局异常处理函数并触发 $AppCrashed 事件:

  1. 在 SensorsAnalyticsSDK 类的 - initWithConfigOptions:debugMode: 方法中初始化 SensorsAnalyticsExceptionHandler 类的单例对象:

3.3. 方案优化

在实际开发过程中,可能会集成多个 SDK,如果这些 SDK 都按照上面介绍的方法采集异常信息,总会有一些 SDK 采集不到异常信息。这是因为通过 NSSetUncaughtExceptionHandler 函数设置的是一个全局异常处理函数,后面设置的异常处理函数会自动覆盖前面设置的异常处理函数。

那么如何解决这个问题呢?

常见的做法是:在调用 NSSetUncaughtExceptionHandler 函数设置全局异常处理函数之前,先通过 NSGetUncaughtExceptionHandler 函数获取之前已设置的异常处理函数并保存,在处理完异常信息后,再主动调用已保存的处理函数,即可解决上面提到的覆盖问题。

  1. 新增一个 NSUncaughtExceptionHandler 类型的属性 defaultExceptionHandler ,用来保存之前已经设置的异常处理函数:

  1. 触发 $AppCrashed 事件后调用之前已设置的异常处理函数,传递 UncaughtExceptionHandler :

通过上面的处理,即可把所有的异常处理函数形成链条,确保之前设置的异常处理函数也能采集到异常信息。

4. Unix 信号异常采集

4.1. 方案简介

在 iOS 应用程序中,一般情况下会采集 SIGILL、SIGABRT、SIGBUS、SIGFPE 和 SIGSEGV 这几个常见的信号,即能满足日常采集应用程序异常信息的需求。我们可以先新增信号处理函数,然后注册信号处理函数,使用 Unix 信号信息构造一个 NSException 对象,复用上节采集 $AppCrashed 事件的方法。

4.2. 具体实现

  1. 新增捕获 Unix 信号的处理函数:

  1. 注册信号处理函数:

注意:由于 Unix 信号异常对象是我们自己构建的,因此没有堆栈信息,这里默认获取当前线程的堆栈信息。上节 - sa_handleUncaughtException: 方法中已经处理该逻辑。

4.3. 方案优化

同样,为了避免影响其他 SDK 捕获 Unix 信号,我们应当在处理 Unix 信号之前保存已经设置的 Unix 信号异常处理函数。然后,在处理完异常信息后再主动调用保存的 Unix 信号异常处理函数。传递 Unix 信号的逻辑与上节传递 UncaughtExceptionHandler 类似。

  1. 新增一个属性 prev_signal_handlers ,用来保存之前已经设置的 Unix 信号异常处理函数:

  1. 触发 $AppCrashed 事件后向之前保存的异常处理函数传递 Unix 信号并调用:

注意:如果其他 SDK 在处理 Unix 信号时忽略了某个信号,那么在触发 $AppCrashed 事件后应当避免向其传递忽略的 Unix 信号,我们在调用 sa_handler 函数时做了判断以处理该逻辑。

5. 补发退出事件

一旦程序发生异常,我们就采集不到 App 退出事件($AppEnd)。这样会造成在用户的行为序列中,出现 App 启动事件($AppStart)和 App 退出事件($AppEnd)不成对的情况。因此,在应用程序发生崩溃时,我们需要补发 $AppEnd 事件:

在进行这样的处理之后,当应用程序发生异常时,我们不仅可以采集 $AppCrashed 事件,还能正常采集 $AppEnd 事件。

6. 总结

本文主要介绍了神策分析 iOS SDK 崩溃采集模块的具体实现。SDK 崩溃采集涵盖了 NSException 异常和 Unix 信号异常,详细的实现可以参考 iOS SDK 源码

最后,希望通过这篇文章,大家能够对神策分析 iOS SDK 的崩溃模块有一个系统的了解。

7. 参考文献

  1. https://developer.apple.com/documentation/foundation/nsexception

  2. https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Exceptions/Tasks/HandlingExceptions.html#//apple_ref/doc/uid/20000059-SW1

  3. https://mp.weixin.qq.com/s/hOOzVzJ-nAtkQ8iD-8wVGg

  4. https://zh.wikipedia.org/wiki/%E5%96%AE%E4%B8%80UNIX%E8%A6%8F%E7%AF%84

  5. https://blog.51cto.com/arthurchen/736181

  6. https://github.com/sensorsdata/sa-sdk-ios

文章来源:公众号神策技术社区


发布于: 19 小时前阅读数: 4
用户头像

公众号:神策技术社区 2021.03.09 加入

公众号:神策技术社区

评论

发布
暂无评论
如何找到程序崩溃的 “凶手” ?