1. 崩溃工程基础

崩溃(Crash)是软件运行时因非法操作或未捕获异常导致的非正常终止。理解崩溃类型、日志结构与平台差异,是设计采集、归因与修复流水线的第一道门槛。本章从技术实现角度拆解这三个基础模块,后续所有章节的落地方案都建立在此之上。

1.1 崩溃的核心分类

从底层触发机制和上层暴露形式,崩溃可分为三大类:

崩溃类别 典型原因 特征指标 常见平台
信号级(Signal / Fault) SIGSEGV(空指针/越界)、SIGABRT(断言失败)、SIGFPE(除零) unix 信号编号 + fault address + 指令寄存器 Linux / Android / iOS / macOS
异常级(Exception) Java NullPointerException、C++ std::bad_alloc、Objective-C NSException 异常类型 + message + throw stack Android / iOS / Windows
资源/状态级 OOM(内存耗尽)、Watchdog 超时、UI 线程阻塞 ANR 内存快照/时间戳/主线程调用栈 Android / iOS / macOS
工程判断口诀:信号级查指针与内存;异常级查业务逻辑与第三方库;资源级查泄漏与并发瓶颈。三类崩溃的采集与归因策略有显着差异,不可混用同一套解析流程。

1.2 崩溃日志的标准化结构

一份完整的崩溃日志通常包含以下六个域,缺一不可。采集端需要保证每个域的完整性,归因端依赖这些域进行符号化与上下文推理。

  1. Header:app 版本、build id、操作系统版本、设备型号、语言、启动时间。
  2. 异常信息:信号编号 / 异常类型 / 异常描述、触发线程 id、挂起线程列表。
  3. 调用栈(Stack Trace):线程栈帧列表(地址 + 符号名 + 源文件:行号),通常包含未符号化的 memory addresses。
  4. 寄存器快照:关键寄存器值(PC, LR, SP, FP, Exception Address),用于恢复执行现场。
  5. 应用状态:最近用户行为序列、当前 activity/controller 栈、网络状态、电量等。
  6. 二进制映射:加载的动态库列表及其加载基址,用于地址符号化回退。

实战日志示例(iOS Mach Exception → JSON 结构)

{
  "header": {
    "app_version": "2.3.1",
    "os_version": "16.5",
    "device": "iPhone14,2",
    "build_id": "A12345"
  },
  "exception": {
    "type": "EXC_BAD_ACCESS",
    "code": "KERN_INVALID_ADDRESS",
    "address": "0x0000000100abc000",
    "thread_id": 5632
  },
  "stacktrace": [
    {"frame": 0, "address": "0x1000e3f4c", "symbol": "-[UIView layoutSublayers] + 68"},
    {"frame": 1, "address": "0x1000e2a10", "symbol": "CALayerLayout + 144"},
    {"frame": 2, "address": "0x1a03b4e08", "symbol": "CA::Context::commit_transaction"}
  ],
  "binary_images": [
    {"name": "MyApp", "base": "0x100000000", "uuid": "ABCD-1234"},
    {"name": "CoreFoundation", "base": "0x1a0200000", "uuid": "EFGH-5678"}
  ]
}

1.3 常见平台差异与采集策略

不同操作系统对崩溃的捕获机制、日志格式与符号化流程有本质差异。归因系统必须根据平台选择适配器(adapter)进行归一化处理。

维度 Android (Java/Kotlin + Native) iOS / macOS (Objective-C + Swift) Linux (C/C++)
异常捕获机制 Thread.setDefaultUncaughtExceptionHandler + signal(SIGSEGV) 监听 NSSetUncaughtExceptionHandler + mach_exception_self 端口 signal() 或 sigaction() + SA_SIGINFO
日志格式 Java stack trace + logcat + tombstone(native) Exception call stack + CrashReport plist / JSON core dump + dmesg + backtrace() 输出
符号化方案 ProGuard mapping + addr2line + llvm-symbolizer dSYM Bundle + atos / lldb DWARF debug info + addr2line / gdb
稳定性保障 Java 层 OOM 后信号处理可能不可用;需 fork 子进程采集 Mach handler 在守护线程中运行,不阻塞主线程 信号处理函数必须可重入,禁止使用 malloc/printf
常见工程陷阱:
- iOS 上使用 NSUncaughtExceptionHandler 只能捕获 Objective-C 异常,无法捕获 Mach 信号(如 EXC_BAD_ACCESS),必须同时注册 Mach exception 端口;
- Android Native 崩溃通过 debuggerd 生成 tombstone 文件,应用进程不能再写文件,建议使用 pipe 或 socket 传递数据;
- Linux 上信号处理函数中调用非 async-signal-safe 函数(如 free、log)会导致死锁,日志采集应使用 write() 直接写入预分配的 ring buffer。

1.4 崩溃工程整体流程

下图展示了从崩溃触发到灰度验证的完整链路。本章聚焦前两个阶段(采集与归一化),后续章节分别拆解每个子步骤的实现细节。

崩溃工程完整链路 崩溃触发 信号/异常/资源 日志生成与捕获 handler + 上下文 日志解析与聚合 符号化 + 归因 归因与定位 特征库 + 聚类 修复与灰度验证 补丁 + 实验 灰度发布与监控 分批次+自动回滚 本章聚焦前两段(崩溃触发 → 日志生成与捕获),后续章节逐一拆解解析、归因、修复与灰度

1.5 日志采集的最小可行实现策略

结合平台差异,设计采集模块时应遵循以下原则:

  • 多级挂载:同时注册 UncaughtExceptionHandler + 信号监听,防止一种方式失效后无日志可用。
  • 局部隔离:采集进程与业务进程分离(fork 子进程写日志),避免业务状态污染日志。
  • 自愈与降级:若日志文件写入失败(磁盘满/权限问题),退而使用 mmap 环形缓冲区,或通过网络 socket 直传。
  • 隐私剪裁:在采集端预先过滤路径、用户 ID、 等敏感信息,避免日志泄露隐私。
实验代码片段:
// 混合注册示例(精简 C 代码)
void register_crash_handlers() {
    // 1. 注册信号处理
    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = crash_signal_handler;
    sa.sa_flags = SA_SIGINFO;
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGABRT, &sa, NULL);
    
    // 2. 线程挂起监听(iOS Mach)
    mach_port_t self = mach_task_self();
    exception_mask_t mask = EXC_MASK_BAD_ACCESS | EXC_MASK_CRASH;
    mach_port_t exception_port;
    mach_port_allocate(self, MACH_PORT_RIGHT_RECEIVE, &exception_port);
    task_set_exception_ports(self, mask, exception_port,
                             EXCEPTION_DEFAULT, THREAD_STATE_NONE);
}