找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2097

积分

0

好友

301

主题
发表于 2025-12-25 17:15:36 | 查看: 33| 回复: 0

图片

应用上线后,崩溃是开发者最不愿面对的问题之一。线下测试稳定的应用,为何线上会出现崩溃?崩溃日志又是如何采集的?常见的编程疏忽往往是罪魁祸首。

典型崩溃场景分析:

  • 数组越界:访问数组时索引超出范围。
  • 多线程问题:在子线程更新UI,或数据读写时机冲突(如一个线程置空数据时另一线程正在读取)。
  • 主线程无响应:主线程长时间阻塞会被系统Watchdog终止。
  • 野指针:访问已释放对象的内存区域。

为了系统性地解决这些问题,深入理解并构建有效的监控方案至关重要。

iOS异常体系分层解析

iOS的异常处理是一个从硬件到应用的层次化体系。理解这一架构,是设计有效监控方案的基础。

  1. 硬件层异常

    • CPU异常:由硬件直接触发,如非法指令、内存访问错误,是所有异常的源头。
  2. 系统层异常

    • Mach异常:macOS/iOS系统最底层的异常机制,源于Mach微内核架构
    • Unix信号:Mach异常通常会被转换为Unix信号,如SIGSEGV、SIGABRT。这是应用层监控的主要捕获点。
  3. 运行时层异常

    • NSException:Objective-C运行时抛出的异常,如数组越界、空指针。
    • C++异常:C++代码抛出的异常,最终通过std::terminate()处理。
  4. 应用层异常

    • 业务逻辑异常:应用自定义的错误。
    • 性能异常:如主线程死锁、内存泄漏。
    • 僵尸对象访问:访问已释放对象导致的异常。

图片

异常捕获的传递链条:

  1. 硬件异常被Mach内核捕获,转为Mach异常消息。
  2. Mach异常可被转换为对应的Unix信号。
  3. 未处理的NSException或C++异常会触发系统层信号(如SIGABRT)。
  4. 应用层异常需主动监控。

监控策略对应:

  • 系统层监控:通过捕获Mach异常和Unix信号,覆盖所有底层崩溃。
  • 运行时监控:设置NSUncaughtExceptionHandler和C++ terminate handler
  • 应用层监控:主动实现死锁检测、僵尸对象检测等机制。

主流异常监控方案选型:KSCrash

iOS侧异常监控领域,PLCrashReporter与KSCrash是两个主流选择。两者均开源且经过生产环境检验。

图片

综合对比,KSCrash的核心优势在于:

  • 监测全面:同时支持C++异常、死锁检测、僵尸对象检测。
  • 异步安全:崩溃处理流程设计为完全异步安全,采用双重异常处理线程确保可靠性。
  • 技术先进:具备堆栈游标抽象、内存内省、模块化架构等技术优势。

因此,选择基于KSCrash构建崩溃监控的核心方案。

基于KSCrash的监控方案实现

架构设计
异常采集模块是数据采集层的一部分,其分层架构如下:
图片

  • 监控器管理层:统一管理各类监控器。
  • 异常捕获层:多种监控器分别捕获Mach异常、信号、运行时异常等。
  • 异常处理层:构建崩溃上下文,收集堆栈、内存等信息。
  • 报告生成层:将上下文转换为JSON报告。

以下详述各层异常捕获原理。

系统层异常捕获:Mach异常与Unix信号

1. Mach异常捕获
Mach异常是系统最底层的异常。监控步骤如下:
图片

  • 创建异常端口:申请一个Mach端口作为异常消息的接收通道。创建前需保存旧端口以保证兼容性。
    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);
  • 注册异常处理器:将任务(进程)的异常端口设置为新端口。
    task_set_exception_ports(mach_task_self(), EXC_MASK_ALL, g_exceptionPort, EXCEPTION_DEFAULT, MACHINE_THREAD_STATE);
  • 创建异常处理线程:为防止处理线程自身崩溃,需创建主、备两个独立线程。备用线程平时挂起,主线程工作时将其恢复。
    图片
  • 处理异常消息:线程通过mach_msg()接收异常消息,随后挂起所有线程、收集机器状态、构建异常上下文并统一处理。

2. Unix信号捕获
作为Mach异常的补充,直接捕获信号能防止Mach处理失败时漏报。信号可能来自未处理的Mach异常,也可能直接由abort()等调用产生。
图片

  • 安装信号处理器
    struct sigaction action = {{0}};
    action.sa_flags = SA_SIGINFO | SA_ONSTACK;
    action.sa_sigaction = &signal_handle_signals;
    sigaction(fatal_signal, &action, &previous_signal_handler);
  • 信号处理函数:获取信号详情(编号、代码、地址)和CPU上下文后,转入与Mach异常相同的处理流程。

3. 机器上下文与堆栈还原
崩溃后需从CPU寄存器和堆栈内存还原调用栈。每个函数调用会形成一个堆栈帧,包含返回地址、帧指针(FP)等。
图片
堆栈遍历从程序计数器(PC)和链接寄存器(LR)开始,随后通过帧指针链在内存中回溯。
图片
关键点:需安全访问内存、检测堆栈溢出、对不同CPU架构的地址进行规范化处理。

运行时异常捕获:NSException与C++异常

1. NSException捕获
通过设置NSUncaughtExceptionHandler捕获未处理的Objective-C异常。

NSUncaughtExceptionHandler *previous = NSGetUncaughtExceptionHandler();
NSSetUncaughtExceptionHandler(&handle_uncaught_exception);

处理完成后,应调用保存的旧handler,并最终调用abort()终止程序。可通过[exception callStackReturnAddresses]获取调用栈地址。

2. C++异常捕获
通过设置C++ terminate handler来拦截未捕获的异常。

std::terminate_handler original = std::get_terminate();
std::set_terminate(cpp_exception_terminate_handler);

图片
当异常未被捕获时,std::terminate()会被调用,进而执行我们设置的handler。处理完毕后需调用原始的handler。

应用层异常捕获:主动检测

1. 主线程死锁检测
通过独立的监控线程和“心跳”机制实现。
图片

  • 监控线程定期向主队列派发一个“心跳”任务。
  • 若在设定超时时间内未得到响应,则判定为死锁。
  • 注意:需合理设置超时阈值,避免因主线程执行长任务而产生误报。

2. 僵尸对象检测
僵尸对象指已被释放但指针仍被访问的对象,常导致EXC_BAD_ACCESS崩溃。主要原因包括unsafe_unretained指针、多线程竞争、桥接不当等。
检测思路:

  • Hook NSObjectNSProxydealloc 方法。
  • 对象释放时,计算其哈希值并记录类信息。
  • 当异常发生时,检查该地址是否曾记录为已释放对象。
    图片
  • 权衡设计:为控制开销,通常限定记录数量上限(如32768个),且哈希冲突可能导致误报。

运行时符号化与异步安全

1. 运行时符号化
崩溃堆栈是内存地址,需转换为可读信息。dladdr()可用于运行时获取函数名和镜像名。

  • 地址调整:堆栈存储的是返回地址,需减去指令偏移量才能得到调用地址。例如ARM64架构:uintptr_t address = (return_address &~ 3UL) - 1;
    图片

2. 异步安全
在信号或Mach异常处理函数中,必须使用异步安全函数。因为崩溃时系统状态不稳定,堆可能已损坏,调用mallocfreeNSLog或Objective-C方法可能导致死锁或二次崩溃。

总结与展望

本文系统介绍了iOS异常监控的分层体系、基于KSCrash的方案选型,以及各层异常(Mach、信号、NSException、C++异常、死锁、僵尸对象)的捕获实现原理。监控能力仍在持续演进,未来可探索实时上传、崩溃回调、结合App日志、dump寄存器附近内存等优化方向。该方案已应用于阿里云用户体验监控RUM iOS SDK中。




上一篇:Chrome 142 CSS样式查询范围语法详解:实现数值阈值响应式设计
下一篇:摩尔线程MUSA统一架构与花港GPU技术全景:发布万卡智算集群与多款芯片
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-11 11:55 , Processed in 0.290504 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表