图3崩溃信息描述
那Bugly为什么收集不到这种崩溃?
(1)信号类型
首先,信号是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。因此在应用的Crash引起的程序异常退出都会有signal。它的种类有多种,常见的有SIGSEGV,SIGILL,SIGABRT,SIGBUS,SIGKILL等等。
信号类型 | 信号解释 |
SIGSEGV | 无效的内存地址引用信号,试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。 |
SIGILL | 执行了非法指令,通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。 |
SIGABRT | 通常由于异常引起的中断信号,异常发生时系统会调用abort函数发出该信号。 |
SIGBUS | 非法地址, 包括内存地址对齐(alignment)出错。与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。 |
SIGKILL | 用来立即结束程序的运行,该信号不能被阻塞、处理和忽略。 |
表 1 信号类型解释
其中本次发生崩溃的信号是终止程序的SIGKILL,它是不能被阻塞、处理和忽略。因此在应用中不能捕获此类崩溃,第三方工具中是无法收集到。
(2)Code异常编码
异常编码也是分析崩溃原因的重要依据之一,该日志中Code码0x8badf00d,即“ate bad food”,表示在应用程序启动、终止或响应系统事件花费的时间过长,应用程序已被系统终止,发生了监视程序超时。它是苹果设计的“看门狗”(watchdog)机制,若超出了该场景所规定的运行时间,“看门狗”就会强制终结这个应用的进程。
触发0x8badf00d的场景除了主线程被卡死的情况,还有以下几种情况:
在iOS11.0到iOS11.2以前系统手机在前台收到推送后进入后台被*死或可能会在前台*死。
开启任务后做了大量耗时操作无法任务结束。
系统挂起beginBackgroundTask方法回调中没有关闭后台任务或添加两次或两次以上的回调无法一对一关闭后台任务。
开启任务后在到期事件处理的回调中开启子线程进行大量耗时操作等等。
因此以上的场景均无法应用拦截,处理,不能上报到第三方崩溃收集工具中。
借助隐私数据查询崩溃日志
既然第三方崩溃收集工具拿不到日志,那么我们之前是通过将iPhone设备连接到电脑中,通过”Xcode-Devices and Simulators-View Devices Logs”来导出当前设备发生的崩溃日志。这种方式可以收集到所有类型的崩溃。但是不可能人人都具备Xcode工具,也不可能时时刻刻都带电脑。而我们发现苹果会将当前设备所发生的所有事件都记录到系统日志中,包括崩溃日志,CPU Usage日志。
在系统日志中崩溃日志名称的格式为“进程名 日期 时间.ips.synced”或“进程名 日期 时间.ips”,如:“58tongcheng-2019-12-04-113614.ips”。该日志在iOS10.2以及以上系统的设备上可以进入“设置-隐私-诊断与用量”中获取,iOS10.2以上系统的设备上可以进入“设置-隐私-分析-分析数据”中获取。因此,用户可以直接通过iPhone设备选择一个崩溃日志后,通过Airdrops或其他三方app发送到电脑或崩溃自动解析工具进行解析。
图4系统隐私数据
点对点分析崩溃日志
在获取到日志之后如何进行解析呢?针对指定的日志进行日志解析,绝大多数iOS开发者都会想到使用符号表进行解析。但是原始的dSYM文件可能存在没有保存或者丢失的情况。因此58同城对日志解析进行了相应的扩展,扩大了日志的解析的适用范围。除了使用原始的dSYM符号表文件进行日志解析外,58的点对点日志解析工具还支持,针对bugly生成的符号表symbol文件的解析,甚至在没有任何符号表的情况下,也可以根据二进制数据进行日志解析。
基于dSYM符号表
众所周知,崩溃日志符号化所需要的符号表通常指dSYM文件,dSYM文件是用来记录调试信息的文件,其数据存储格式为DWARF格式。其数据来源为应用二进制文件的DEBUG段,记录的信息主要包括:文件路径信息、行号信息、变量与地址的映射、函数与地址的映射等。正是因为其存在地址与符号的映射关系,符号表才可以被用于解析崩溃日志。在得到崩溃日志和相应的dSYM文件后,可借助symbolicatecrash工具实现日志符号化。如果没有symbolicatecrash工具,那么dwarfdump命令也可以逐条实现地址符号化。
在业务开发过程中,本地调试状态下打包是默认不生成dSYM文件的,但是这并不意味着调试信息和符号信息丢失了。当我们本地Xcode打出来的包发生偶现崩溃时,可以通过Xcode提供的dsymutil工具将dSYM文件从应用程序的二进制文件中剥离。剥离出的dSYM文件即可借助相应symbolicatecrash实现地址符号化。
基于bugly符号表
在使用bugly进行崩溃统计时,我们需要将符号表上传到bugly的后台。这个符号表并不是原始的dSYM文件,而是bugly从dSYM文件中提取的文本文件。其数据格式如下图所示:
图5 Symbol文件
bugly的符号表是bugly从dSYM文件中提取的函数地址与符号的映关系,其格式为:起始指令地址 结束指令地址 代码所在函数名 代码所在文件及行号。举例说明,假如我们拿到的崩溃偏移地址为B,通过文本扫描后发现函数F的L行代码的起始指令地址为A,结束指令地址C,地址满足A <= B <= C的原则,因此可以确定崩溃发生在F函数的L行。由于bugly的符号表只保留了函数地址符号映射,不包含文件路径、变量地址符号映射等信息,因此bugly的符号表相比于dSYM文件更轻量,更适合保存和传输。
无符号情况处理
58同城在业务开发阶段提供给测试同学的测试包都是通过Jenkins服务打包。随着业务的发展,58同城APP的体积越来越庞大,这就导致测试同学从Jenkins服务器上下载APP的时间较长。为了能够尽可能的减小下载体积,58同城将APP的符号表在打包期间从应用程序中剥离出来形成dSYM文件,保存在打包服务器中。因此测试同学下载的Jenkins包是不包含符号表信息的。由于剥离出来的dSYM文件较大,为了节省服务器空间,dSYM在保留2天后会自动清除。假设有这样一个场景:测试同学下载了一个测试包,在测试到第三天时发生了不可稳定复现的崩溃,那么此时我们进行日志解析是没有任何符号表的。
为了解决这种场景的问题,58同城开发了基于Mach-O文件解析的无符号表日志解析工具。通过遍历二进制文件中所有类的方法列表,确定崩溃堆栈的指令地址位于哪个函数的指令区间范围,从而确定崩溃发生时正在调用的函数,进而实现崩溃日志的符号化。目前此工具已经成为58质量保证必不可缺的工具之一。相关代码已经通过58技术委员会审核,近期将对外开源。
分析崩溃堆栈
因此,通过点对点崩溃分析的方式将崩溃日志进行解析,我们获取了具体各个不同线程的堆栈信息,开始定位问题。
该崩溃主要现象是主线程卡死,我们先从主线程的堆栈开始分析。
主线程调用栈分析
图6 崩溃主线程堆栈信息
日志中,应用被*死之前主线程停留在 [WIMOpenUDID valueWithError:]中获取系统剪贴板UIPasteboard对象的操作中。但是通常情况下,在主线程中获取一个对象不会把主线程卡死,于是我们便查看了这个方法的实现以尝试定位问题。如下: