<p>作者:高玉龙(元泊)</p>
背景介绍
App 上线后,作为开发同学,最怕出现的情况就是应用崩溃了。但是,线下测试好好的 App,为什么上线后就发生崩溃了呢?这些崩溃日志信息是怎么采集的?
先看看几个常见的编写代码时的疏忽,是如何让应用崩溃的。
- 数组越界:在取数据索引时越界,App 会发生崩溃。
- 多线程问题:在子线程中进行 UI 更新可能会发生崩溃。多个线程进行数据的读取操作,因为处理时机不一致,比如有一个线程在置空数据的同时另一个线程在读取这个数据,可能会出现崩溃情况。
- 主线程无响应:如果主线程超过系统规定的时间无响应,就会被 Watchdog 杀掉。
- 野指针:指针指向一个已删除的对象访问内存区域时,会出现野指针崩溃。
为了解决这个问题,阿里云可观测研发团队进行了一些 iOS 异常监控方向的探索。
iOS 异常体系介绍
iOS 异常体系采用分层架构,从底层硬件到上层应用,异常在不同层次被捕获和处理。理解异常体系的分层结构,有助于我们更好地设计和实现异常监控方案。iOS 异常体系主要分为以下几个层次:
1. 硬件层异常
- CPU 异常:由硬件直接产生的异常,如非法指令、内存访问错误等
- 这是最底层的异常来源,所有其他异常最终都源于此
2. 系统层异常
- Mach 异常: macOS/iOS 系统最底层的异常机制,源于 Mach 微内核架构
- Unix 信号: Mach 异常会被转换为 Unix 信号,如 SIGSEGV、SIGABRT 等
- 系统层异常是应用层异常监控的主要捕获点
3. 运行时层异常
- NSException: Objective-C 运行时异常,如数组越界、空指针等
- C++ 异常: C++ 代码抛出的异常,通过
std::terminate()处理 - 运行时层异常通常由编程错误引起
4. 应用层异常
- 业务逻辑异常:应用自定义的异常和错误
- 性能异常:主线程死锁、内存泄漏等
- 僵尸对象访问:访问已释放对象导致的异常
异常体系的分层关系如下图所示:

异常捕获的层次关系:
- 硬件异常 → Mach 异常: CPU 异常被 Mach 内核捕获,转换为 Mach 异常消息
- Mach 异常 → Unix 信号: Mach 异常处理机制会将异常转换为对应的 Unix 信号
- 运行时异常: NSException 和 C++ 异常在运行时层被捕获,如果未处理会触发系统层异常
- 应用层异常: 业务异常和性能问题需要应用层主动监控和检测
异常监控策略:
- 系统层监控: 通过 Mach 异常和 Unix 信号捕获,可以捕获所有底层异常
- 运行时层监控: 通过设置异常处理器(NSUncaughtExceptionHandler、terminate handler)捕获运行时异常
- 应用层监控: 通过主动检测机制(死锁检测、僵尸对象检测)发现潜在问题
理解这个分层体系,有助于我们:
- 选择合适的异常捕获机制
- 理解不同异常类型的来源和处理方式
- 设计完整的异常监控方案
主流异常监控方案
在 iOS 端侧异常监控领域,PLCrashReporter 与 KSCrash 是最常用的两个内核库。两者都是开源、生产可用,且被多家平台化产品或 SDK 采用作为底层能力。

基于以上对比分析,KSCrash 相比其他崩溃监控框架的核心优势在于:
- 异常类型监测支持更全面(唯一同时支持 C++ 异常、死锁检测、僵尸对象检测的开源框架)
- 异步安全设计(崩溃处理完全异步安全,双重异常处理线程确保可靠性)
- 技术优势明显(堆栈游标抽象、内存内省、模块化架构等)
基于以上优势,我们选择基于 KSCrash 作为崩溃异常监控的核心方案。
异常监控方案实现
架构设计
异常采集模块,是我们 SDK 数据采集层一个模块的具体实现,如下:

- 监控器管理层:统一管理所有监控器,提供统一的异常处理入口
- 异常捕获层:多种监控器,分别捕获不同类型的异常和状态信息
- 异常处理层:构建崩溃上下文,收集堆栈、符号、内存等信息
- 报告生成层:将崩溃上下文转换为 JSON 格式报告
接下来,我们介绍各种类型异常的捕获原理,以及对应监控器是如何实现的。
系统层异常捕获
系统层异常包括 Mach 异常和 Unix 信号,是应用层异常监控的主要捕获点。我们需要同时捕获这两种异常,确保不遗漏任何底层异常。
Mach 异常捕获
Mach 异常是 macOS/iOS 系统最底层的异常机制,源于 Mach 微内核架构。Mach 是 macOS/iOS 内核的基础,提供了进程间通信(IPC)和异常处理的核心机制。硬件异常(CPU 异常)会被 Mach 内核捕获并转换为 Mach 异常消息。Mach 异常与特定线程关联,可以精确捕获异常发生的线程。Mach 异常通过 Mach 消息异步传递异常信息,需要使用 Mach 端口(Mach Port)作为异常处理的通信通道。

监控 Mach 异常,涉及以下几个核心的步骤:
1. 创建异常端口
// 创建新的异常处理端口
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &g_exceptionPort);
// 申请端口权限
mach_port_insert_right(mach_task_self(), g_exceptionPort, g_exceptionPort, MACH_MSG_TYPE_MAKE_SEND);
为了与三方 SDK 兼容,在创建新的异常处理端口之前,需要对旧的异常处理端口进行保存,并在异常处理完毕后恢复旧的异常端口。
2. 注册异常处理器
把异常处理端口设置为刚才创建的:
// 设置异常端口,捕获所有异常类型
task_set_exception_ports(
mach_task_self(),
EXC_MASK_ALL,
g_exceptionPort,
EXCEPTION_DEFAULT,
MACHINE_THREAD_STATE
);
3. 创建异常处理线程
为了防止异常处理线程本身崩溃,需要创建两个独立的异常处理线程:
-
主处理线程:正常处理异常
-
备用处理线程:主处理线程崩溃时的后备份方案
// 主异常处理线程 pthread_create(&g_primaryPThread, &attr, handleExceptions, kThreadPrimary); // 备用异常处理线程(防止主线程崩溃) pthread_create(&g_secondaryPThread, &attr, handleExceptions, kThreadSecondary);
主备线程之间的关系如下:

- 备用处理线程在创建后会立即挂起
- 主线程在处理异常之前会通过
thread_resume()函数恢复备用处理线程 - 备用处理线程恢复后,会进入
mach_msg()等待 - 如果主线程在处理异常时发生崩溃,备用处理线程可以继续处理崩溃信息(由于异常端口已恢复,此时备用线程可能也收不到消息)
4. 处理异常消息
异常处理线程通过 mach_msg() 接收异常消息:
mach_msg_return_t kr = mach_msg(
&exceptionMessage.header,
MACH_RCV_MSG | MACH_RCV_LARGE,
0,
sizeof(exceptionMessage),
g_exceptionPort,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL
);

- 挂起所有线程:确保状态一致性
- 标记已捕获异常:进入异步安全模式
- 激活备用处理线程
- 读取异常线程的机器状态
- 初始化堆栈游标
- 构建异常上下文
- 异常类型
- 机器状态
- 地址信息等
- 堆栈游标
- 统一异常处理:不同异常类型统一处理
- 恢复线程