理解崩溃的底层原理是有效治理的基础。本文详细介绍iOS的异常处理机制、崩溃的传递链路以及各类崩溃的本质原因。


iOS异常处理架构

iOS的异常处理采用分层架构,从底层到上层依次为:

flowchart BT
    subgraph ARCH["异常处理架构"]
        direction BT
        HW["硬件层 (Hardware)
CPU异常、内存访问违规、非法指令"] MACH["Mach层 (Mach Exception)
EXC_BAD_ACCESS、EXC_BAD_INSTRUCTION、EXC_CRASH"] BSD["BSD层 (Unix Signal)
SIGABRT、SIGSEGV、SIGBUS、SIGFPE、SIGILL"] APP["应用层 (Application Layer)
NSException、@try-@catch、Swift Error"] HW --> MACH MACH --> BSD BSD --> APP end

需要注意:这张图表示崩溃治理中常见的异常/终止机制层次,并不表示所有异常都一定从硬件层逐级向上传递。NSException 属于 Objective-C 运行时/框架层的语言异常机制,不是内核产生的 Mach 异常。只有当 NSException 未被捕获时,运行时最终通常会调用 abort() 主动终止进程,随后表现为 SIGABRT / EXC_CRASH


崩溃的传递链路

当异常发生时,会按照特定的链路传递:

flowchart TD
    A["1. 硬件触发异常
CPU检测到非法操作(如访问无效内存地址)
触发硬件中断,陷入内核态"] --> B["2. Mach层处理
内核将硬件异常转换为Mach异常
通过异常端口发送给用户态的异常处理程序
如果没有处理,转换为Unix信号"] B --> C["3. BSD层处理
Mach异常被转换为对应的Unix信号
调用进程注册的Signal Handler
如果没有处理或处理后继续,执行默认行为"] C --> D["4. 进程终止
默认行为通常是终止进程
生成崩溃日志"]

Mach异常与Unix信号的对应关系

Mach异常Unix信号触发原因
EXC_BAD_ACCESSSIGSEGV/SIGBUS访问无效内存
EXC_BAD_INSTRUCTIONSIGILL非法指令
EXC_ARITHMETICSIGFPE算术异常(如除零)
EXC_BREAKPOINTSIGTRAP断点/调试陷阱
EXC_CRASHSIGABRT程序主动abort

Mach异常机制

Mach内核简介

Mach是macOS/iOS的微内核,提供了最基础的系统服务:

Mach内核提供的核心抽象:

┌─────────────────────────────────────────────────────────────┐
│                      Mach核心概念                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Task(任务)                                                │
│  └── 资源容器,对应一个进程的资源                                │
│      包含虚拟地址空间、端口权限等                                │
│                                                             │
│  Thread(线程)                                              │
│  └── 执行单元,在Task中执行                                    │
│      拥有自己的寄存器状态和栈                                   │
│                                                             │
│  Port(端口)                                                │
│  └── 通信通道,用于进程间通信                                   │
│      异常处理通过端口机制实现                                   │
│                                                             │
│  Message(消息)                                             │
│  └── 通信载体,通过端口发送和接收                                │
│      异常信息以消息形式传递                                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

异常端口

Mach使用异常端口(Exception Port)来处理异常。当Mach异常发生时,内核(XNU内核)会按照特定顺序查找异常处理程序

flowchart TD
    START["异常发生"] --> THREAD
    subgraph LEVEL["异常端口查找顺序(由内核执行)"]
        direction TB
        THREAD["1. Thread Exception Port(线程级)
内核最先检查该线程是否注册了异常端口
极少使用,仅用于极细粒度的单线程调试场景"] TASK["2. Task Exception Port(任务/进程级)
内核接着检查该进程是否注册了异常端口
典型使用者:调试器(LLDB)、崩溃收集SDK(如PLCrashReporter)"] HOST["3. Host Exception Port(主机级)
内核最后检查系统级异常端口
通常是系统默认处理程序(如ReportCrash)"] THREAD -->|未注册或未处理| TASK TASK -->|未注册或未处理| HOST end

这种分层设计的好处是:允许不同粒度的异常处理。线程级可以处理特定线程的异常,进程级可以统一处理整个App的异常,主机级则作为兜底处理。

深入了解Mach端口操作、消息机制以及如何实现自定义异常处理器,请参考 崩溃-Mach异常

注册异常端口

开发者可以通过Mach API注册自己的异常端口:

// 在Task级别注册异常端口(崩溃收集SDK常用方式)
kern_return_t task_set_exception_ports(
    task_t task,                    // 目标任务
    exception_mask_t exception_mask, // 要捕获的异常类型
    mach_port_t new_port,           // 新的异常端口
    exception_behavior_t behavior,   // 异常行为
    thread_state_flavor_t flavor     // 线程状态类型
);

// 在Thread级别注册异常端口
kern_return_t thread_set_exception_ports(
    thread_t thread,
    exception_mask_t exception_mask,
    mach_port_t new_port,
    exception_behavior_t behavior,
    thread_state_flavor_t flavor
);

常见Mach异常类型

EXC_BAD_ACCESS
├── 最常见的崩溃类型
├── 访问了无效的内存地址
├── 常见原因:
│   ├── 野指针访问
│   ├── 对象被释放后访问
│   ├── 数组越界(底层)
│   └── 栈溢出
└── 对应信号:SIGSEGV 或 SIGBUS

EXC_BAD_INSTRUCTION
├── 执行了非法指令
├── 常见原因:
│   ├── Swift强制解包nil
│   ├── 调用了纯虚函数
│   └── 代码段损坏
└── 对应信号:SIGILL

EXC_ARITHMETIC
├── 算术异常
├── 常见原因:
│   ├── 整数除零
│   └── 浮点异常
└── 对应信号:SIGFPE

EXC_BREAKPOINT
├── 断点异常
├── 常见原因:
│   ├── 调试断点
│   ├── Swift的fatalError
│   ├── assert失败
│   └── precondition失败
└── 对应信号:SIGTRAP

EXC_CRASH
├── 程序主动终止
├── 常见原因:
│   ├── 调用abort()
│   ├── 未捕获的NSException
│   └── C++ exception
└── 对应信号:SIGABRT

Unix信号机制

信号基础

Unix信号是一种进程间通信机制,也用于异常通知:

// 信号处理函数原型
typedef void (*sig_t)(int);

// 注册信号处理函数
signal(SIGSEGV, signal_handler);

// 或使用更强大的sigaction
struct sigaction sa;
sa.sa_sigaction = signal_handler_with_info;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, NULL);

常见崩溃信号

信号说明典型原因
SIGABRT6程序终止abort()、未捕获异常
SIGBUS10总线错误内存对齐问题、映射失效
SIGFPE8算术异常除零、溢出
SIGILL4非法指令损坏的代码、强制解包nil
SIGSEGV11段错误访问无效内存
SIGTRAP5断点陷阱断点、assert
SIGKILL9强制终止系统强杀(无法捕获)

信号处理注意事项

在信号处理函数中,只能调用异步信号安全(Async-Signal-Safe)的函数:

// 异步信号安全的函数(可以在Signal Handler中调用)
write()
_exit()
sigaction()
kill()
// ... 等少数函数

// 不安全的函数(不能在Signal Handler中调用)
malloc() / free()      // 可能死锁
printf()               // 使用了锁
NSLog()                // OC方法,不安全
objc_msgSend()         // OC消息发送,不安全

NSException处理

NSException概述

NSException是Objective-C层面的异常机制:

// 抛出异常
@throw [NSException exceptionWithName:NSRangeException
                               reason:@"Index out of bounds"
                             userInfo:nil];

// 捕获异常
@try {
    // 可能抛出异常的代码
    [array objectAtIndex:100];
} @catch (NSException *exception) {
    // 处理异常
    NSLog(@"Caught: %@", exception);
} @finally {
    // 清理代码
}

NSException 本身不是内核异常,也不是 Unix 信号。它是由 Objective-C 代码或 Foundation/UIKit 等框架在用户态主动抛出的对象。它和 Mach 异常的关系主要发生在未捕获之后:未捕获的 NSException 会进入 uncaught exception handler,随后进程通常调用 abort(),这个主动终止动作才会进入信号/Mach 崩溃日志体系,常见表现是 SIGABRTEXC_CRASH

常见NSException类型

NSRangeException
├── 数组/字符串越界访问
└── 示例:[array objectAtIndex:100]

NSInvalidArgumentException
├── 参数无效
├── 示例:
│   ├── [dict setObject:nil forKey:@"key"]
│   └── [NSNull null]作为参数
└── unrecognized selector也属于此类

NSInternalInconsistencyException
├── 内部状态不一致
└── 示例:在错误线程操作UI

NSGenericException
├── 通用异常
└── 示例:在枚举时修改集合

未捕获异常处理

// 设置未捕获异常处理函数
void uncaughtExceptionHandler(NSException *exception) {
    // 获取异常信息
    NSString *name = exception.name;
    NSString *reason = exception.reason;
    NSArray *callStack = exception.callStackSymbols;
    
    // 保存崩溃信息
    // 注意:此时应用即将终止,需要快速处理
}

// 注册处理函数
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);

NSException与信号的关系

flowchart TD
    A["1. 代码抛出NSException"] --> B["2. 查找@catch块"]
    B -->|找到| C["3. 执行@catch块"]
    B -->|未找到| D["4. 调用UncaughtExceptionHandler"]
    D --> E["5. 调用abort()"]
    E --> F["6. 产生SIGABRT信号"]
    F --> G["7. 进程终止"]

内存相关崩溃

野指针(Dangling Pointer)

野指针是指向已释放内存的指针:

// 野指针示例
@implementation MyClass

- (void)example {
    NSObject *obj = [[NSObject alloc] init];
    __unsafe_unretained NSObject *unsafeRef = obj;
    
    obj = nil;  // 对象被释放
    
    // unsafeRef现在是野指针
    [unsafeRef description];  // EXC_BAD_ACCESS
}

@end

野指针崩溃的特点:

  • 难以复现(取决于内存是否被重用)
  • 崩溃位置可能与问题代码相距甚远
  • 堆栈可能不完整或混乱

内存越界

// 栈越界
- (void)stackOverflow {
    char buffer[10];
    // 写入超过缓冲区大小
    strcpy(buffer, "This is a very long string");
}

// 堆越界
- (void)heapOverflow {
    char *buffer = malloc(10);
    // 写入超过分配大小
    memset(buffer, 0, 100);
}

OOM(Out of Memory)

iOS对内存使用有严格限制,超出限制会被系统终止:

iOS内存限制(参考值,因设备而异):

设备类型           前台限制        后台限制
─────────────────────────────────────────
iPhone低端机       ~200MB         ~50MB
iPhone中端机       ~400MB         ~80MB
iPhone高端机       ~1GB+          ~100MB+

注意:
- 这些值是参考值,实际限制取决于系统状态
- 后台应用更容易被终止
- 系统内存压力大时,限制会降低

OOM的特点:

  • 没有崩溃日志(被系统直接终止)
  • 需要通过其他方式检测(如Jetsam日志)
  • 常见于大图片处理、大量数据加载

Watchdog崩溃

什么是Watchdog

Watchdog是iOS的看门狗机制,监控应用的响应性:

Watchdog监控场景:

┌─────────────────────────────────────────────────────────────┐
│                    Watchdog超时场景                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  场景                     超时时间      终止代码               │
│  ─────────────────────────────────────────────────────────  │
│  应用启动                  ~20秒        0x8badf00d           │
│  应用恢复(从后台)          ~10秒        0x8badf00d           │
│  应用挂起                  ~10秒        0x8badf00d           │
│  应用终止                  ~5秒         0x8badf00d           │
│  后台任务                  ~30秒        0xbada5e47           │
│  后台下载                  ~600秒       0xbada5e47           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

0x8badf00d = "ate bad food" = 主线程阻塞
0xbada5e47 = "bad asset"    = 后台任务超时

Watchdog崩溃特征

Watchdog崩溃日志特征:

Exception Type: EXC_CRASH (SIGKILL)
Exception Codes: 0x8badf00d
Exception Note: SIMULATED (这不是真正的崩溃)

Termination Reason: Namespace SPRINGBOARD, Code 0x8badf00d
Termination Description: SPRINGBOARD, 
    scene-create watchdog transgression: xxx exhausted real time allowance of 20.00 seconds

Swift特有的崩溃

Swift的崩溃在不同架构上产生的异常类型有所不同:

Swift崩溃的异常类型(按架构区分):

┌─────────────────────────────────────────────────────────────┐
│  ARM64架构(iOS设备)                                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  EXC_BREAKPOINT (SIGTRAP)                                   │
│  ├── Swift运行时检查失败和主动触发的崩溃都是此类型                 │
│  ├── 触发方式:执行brk指令                                     │
│  ├── 常见场景:                                               │
│  │   ├── 强制解包nil(value!)                                │
│  │   ├── 数组越界访问                                         │
│  │   ├── 强制类型转换失败(as!)                               │
│  │   ├── fatalError()                                       │
│  │   ├── preconditionFailure()                              │
│  │   └── precondition()失败                                  │
│  └── 特点:ARM64上Swift崩溃统一使用brk指令                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  x86_64架构(模拟器)                                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  EXC_BAD_INSTRUCTION (SIGILL)                               │
│  ├── Swift运行时检查失败                                       │
│  ├── 触发方式:执行ud2指令                                     │
│  ├── 常见场景:                                               │
│  │   ├── 强制解包nil(value!)                                │
│  │   ├── 数组越界访问                                         │
│  │   └── 强制类型转换失败(as!)                               │
│  └── 特点:x86上使用ud2(undefined instruction)指令           │
│                                                             │
│  EXC_BREAKPOINT (SIGTRAP)                                   │
│  ├── 开发者主动触发的崩溃                                      │
│  ├── 触发方式:执行int3指令                                    │
│  ├── 常见场景:                                               │
│  │   ├── fatalError()                                       │
│  │   └── preconditionFailure()                              │
│  └── 特点:与调试断点使用相同机制                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

注意:
- 现代iOS设备均为ARM64架构,Swift崩溃主要表现为EXC_BREAKPOINT
- 在模拟器(x86_64)上调试时可能看到EXC_BAD_INSTRUCTION
- assertionFailure()在Release模式下不会触发崩溃

强制解包失败

// Fatal error: Unexpectedly found nil while unwrapping an Optional value
let value: String? = nil
print(value!)  // ARM64: EXC_BREAKPOINT, x86_64: EXC_BAD_INSTRUCTION

数组越界

// Fatal error: Index out of range
let array = [1, 2, 3]
let value = array[10]  // ARM64: EXC_BREAKPOINT, x86_64: EXC_BAD_INSTRUCTION

类型转换失败

// Fatal error: Could not cast value of type 'A' to 'B'
let a: Any = "string"
let b = a as! Int  // ARM64: EXC_BREAKPOINT, x86_64: EXC_BAD_INSTRUCTION

fatalError和precondition

// 主动触发崩溃
fatalError("Something went wrong")  // EXC_BREAKPOINT (所有架构)

// 前置条件检查
precondition(index >= 0, "Index must be non-negative")  // 失败时EXC_BREAKPOINT

崩溃日志解读

崩溃日志结构

典型的iOS崩溃日志结构:

1. Header(头部信息)
   ├── Incident Identifier: 崩溃标识
   ├── Hardware Model: 设备型号
   ├── Process: 进程名和PID
   ├── OS Version: 系统版本
   └── Date/Time: 崩溃时间

2. Exception Information(异常信息)
   ├── Exception Type: 异常类型
   ├── Exception Codes: 异常代码
   ├── Exception Subtype: 子类型
   └── Termination Reason: 终止原因

3. Thread Backtrace(线程堆栈)
   ├── Crashed Thread的完整堆栈
   └── 其他线程的堆栈

4. Binary Images(二进制映像)
   └── 加载的所有动态库和地址范围

示例崩溃日志

Incident Identifier: ABC123-DEF456-GHI789
Hardware Model:      iPhone13,2
Process:             MyApp [12345]
Path:                /private/var/containers/Bundle/Application/.../MyApp.app/MyApp
Identifier:          com.example.MyApp
Version:             1.0.0 (100)
Code Type:           ARM-64
Parent Process:      launchd [1]

Date/Time:           2024-01-15 10:30:45.123 +0800
OS Version:          iOS 17.0 (21A5248v)

Exception Type:      EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype:   KERN_INVALID_ADDRESS at 0x0000000000000010
Exception Codes:     0x0000000000000001, 0x0000000000000010
VM Region Info:      0x10 is not in any region.

Triggered by Thread: 0

Thread 0 Crashed:
0   MyApp                    0x0000000100001234 -[MyClass method] + 56
1   MyApp                    0x0000000100002345 -[MyClass caller] + 128
2   UIKitCore                0x00000001a2345678 -[UIViewController viewDidLoad] + 100
3   ...

崩溃与调试器

调试器对异常的影响

当应用在调试器下运行时,异常处理行为会有所不同:

调试器影响:

1. 异常优先发送给调试器
   └── 调试器可以选择处理或传递

2. 某些崩溃在调试器下不会发生
   └── 如Watchdog超时(调试时禁用)

3. 调试器可能改变内存布局
   └── 某些野指针崩溃可能无法复现

4. 断点异常会被调试器拦截
   └── EXC_BREAKPOINT在调试时不会崩溃

LLDB异常处理

# 查看当前异常处理设置
(lldb) process handle

# 设置SIGSEGV不停止,不通知
(lldb) process handle SIGSEGV --stop false --notify false

# 让所有信号传递给进程
(lldb) process handle --pass true --stop false --notify false