崩溃-信号处理

Unix信号是进程间通信和异常通知的重要机制。本文详细介绍Unix信号的基础知识、常见崩溃信号以及Signal Handler的正确实现方式。 Unix信号基础 什么是信号 信号的本质: ┌─────────────────────────────────────────────────────────────┐ │ Unix信号 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 信号是一种软件中断机制: │ │ • 用于通知进程发生了某个事件 │ │ • 可以由内核、其他进程或进程自身发送 │ │ • 进程可以选择处理、忽略或使用默认行为 │ │ │ │ 信号的来源: │ │ • 硬件异常(如除零、非法内存访问) │ │ • 用户输入(如Ctrl+C) │ │ • 系统调用(如kill) │ │ • 软件条件(如定时器到期) │ │ │ └─────────────────────────────────────────────────────────────┘ 信号的生命周期 flowchart TD A["1. 信号产生(Generation)某个事件触发信号的产生"] --> B["2. 信号投递(Delivery)信号被投递到目标进程此时信号处于pending状态"] --> C["3. 信号处理(Handling)进程响应信号,执行以下之一"] C --> D["执行默认动作"] C --> E["执行自定义处理函数"] C --> F["忽略信号"] 常见信号类型 崩溃相关信号 信号 编号(macOS/iOS) 默认动作 说明 SIGABRT 6 终止+core 调用abort()产生 SIGBUS 10 终止+core 总线错误 SIGFPE 8 终止+core 算术异常 SIGILL 4 终止+core 非法指令 SIGSEGV 11 终止+core 段错误 SIGTRAP 5 终止+core 断点陷阱 SIGSYS 12 终止+core 非法系统调用 其他重要信号 信号 编号 默认动作 说明 SIGKILL 9 终止 强制终止(不可捕获) SIGTERM 15 终止 请求终止 SIGINT 2 终止 中断(Ctrl+C) SIGQUIT 3 终止+core 退出(Ctrl+\) SIGPIPE 13 终止 管道破裂 SIGALRM 14 终止 定时器到期 信号详解 SIGABRT (6) ──────────────────────────────────────── 触发条件: • 调用abort()函数 • 未捕获的NSException • C++ exception未处理 • assert失败(某些情况) 特点: • 可以捕获和处理 • 通常表示程序主动终止 SIGSEGV (11) ──────────────────────────────────────── 触发条件: • 访问无效内存地址 • 空指针解引用 • 栈溢出 • 访问已释放的内存 对应Mach异常: • EXC_BAD_ACCESS (KERN_INVALID_ADDRESS) • EXC_BAD_ACCESS (KERN_PROTECTION_FAILURE) SIGBUS (10) ──────────────────────────────────────── 触发条件: • 内存对齐错误 • 访问不存在的物理地址 • mmap映射失效 与SIGSEGV的区别: • SIGSEGV: 虚拟地址层面的问题(地址未映射或权限不足) • SIGBUS: 物理地址层面的问题(对齐错误、硬件问题等) SIGILL (4) ──────────────────────────────────────── 触发条件: • 执行非法指令 • Swift强制解包nil • 代码段损坏 对应Mach异常: • EXC_BAD_INSTRUCTION SIGFPE (8) ──────────────────────────────────────── 触发条件: • 整数除零 • 浮点异常 • 算术溢出 对应Mach异常: • EXC_ARITHMETIC SIGTRAP (5) ──────────────────────────────────────── 触发条件: • 断点指令 • Swift的fatalError • precondition失败 • 调试器断点 对应Mach异常: • EXC_BREAKPOINT Signal Handler 基本用法 #include <signal.h> // 简单的信号处理函数 void simple_handler(int sig) { // 处理信号 // 注意:这里只能调用异步信号安全的函数 } // 注册信号处理函数 void setup_simple_handler(void) { signal(SIGSEGV, simple_handler); signal(SIGABRT, simple_handler); } 使用sigaction(推荐) #include <signal.h> // 带详细信息的信号处理函数 void detailed_handler(int sig, siginfo_t *info, void *context) { // sig: 信号编号 // info: 信号详细信息 // context: 上下文(包含寄存器状态) } // 使用sigaction注册 void setup_sigaction_handler(void) { struct sigaction action; // 清零结构体 memset(&action, 0, sizeof(action)); // 设置处理函数 action.sa_sigaction = detailed_handler; // 设置标志 action.sa_flags = SA_SIGINFO // 使用sa_sigaction而非sa_handler | SA_ONSTACK; // 使用备用栈 // 清空信号掩码 sigemptyset(&action.sa_mask); // 注册信号处理器 sigaction(SIGSEGV, &action, NULL); sigaction(SIGABRT, &action, NULL); sigaction(SIGBUS, &action, NULL); sigaction(SIGFPE, &action, NULL); sigaction(SIGILL, &action, NULL); sigaction(SIGTRAP, &action, NULL); } siginfo_t结构 typedef struct { int si_signo; // 信号编号 int si_errno; // 错误码 int si_code; // 信号代码(更详细的原因) pid_t si_pid; // 发送信号的进程ID uid_t si_uid; // 发送信号的用户ID void *si_addr; // 故障地址(SIGSEGV/SIGBUS) int si_status; // 退出状态 // ... 其他字段 } siginfo_t; // 解析siginfo void parse_siginfo(int sig, siginfo_t *info) { printf("Signal: %d (%s)\n", sig, strsignal(sig)); printf("Code: %d\n", info->si_code); if (sig == SIGSEGV || sig == SIGBUS) { printf("Fault address: %p\n", info->si_addr); } // 解析信号代码 switch (sig) { case SIGSEGV: switch (info->si_code) { case SEGV_MAPERR: printf("Address not mapped\n"); break; case SEGV_ACCERR: printf("Invalid permissions\n"); break; } break; case SIGBUS: switch (info->si_code) { case BUS_ADRALN: printf("Invalid address alignment\n"); break; case BUS_ADRERR: printf("Nonexistent physical address\n"); break; } break; // ... 其他信号 } } 备用信号栈 为什么需要备用栈 问题场景: 当栈溢出时: 1. 栈空间已耗尽 2. 系统发送SIGSEGV信号 3. 信号处理函数需要栈空间来执行 4. 但是栈已经溢出了! 5. 信号处理函数无法执行 解决方案: 使用备用信号栈(Alternate Signal Stack) 设置备用栈 #include <signal.h> #include <stdlib.h> // 设置备用信号栈 void setup_alternate_stack(void) { // 分配栈空间 void *stack = malloc(SIGSTKSZ); if (stack == NULL) { return; } // 配置备用栈 stack_t ss; ss.ss_sp = stack; ss.ss_size = SIGSTKSZ; ss.ss_flags = 0; // 设置备用栈 if (sigaltstack(&ss, NULL) != 0) { free(stack); return; } // 注册信号处理器时使用SA_ONSTACK标志 struct sigaction action; action.sa_sigaction = signal_handler; action.sa_flags = SA_SIGINFO | SA_ONSTACK; sigemptyset(&action.sa_mask); sigaction(SIGSEGV, &action, NULL); } 异步信号安全 什么是异步信号安全 异步信号安全(Async-Signal-Safe): 信号可能在任何时刻打断正常执行: • 可能在malloc中间被打断 • 可能在printf中间被打断 • 可能持有某个锁时被打断 如果信号处理函数调用了相同的函数: • 可能导致死锁(重复获取锁) • 可能导致数据损坏(数据结构不一致) • 可能导致未定义行为 因此,信号处理函数只能调用"异步信号安全"的函数 异步信号安全函数列表 // 可以在信号处理函数中安全调用的函数(部分): // 文件操作 write() read() open() close() fsync() // 进程控制 _exit() _Exit() abort() raise() kill() getpid() // 信号相关 signal() sigaction() sigprocmask() sigemptyset() sigfillset() sigaddset() sigdelset() // 其他(注意:以下函数不在POSIX异步信号安全列表中,但大多数实现是安全的) strlen() // 大多数实现安全,但非POSIX标准 memcpy() // 大多数实现安全,但非POSIX标准 不安全的函数 // 不能在信号处理函数中调用的函数: // 内存分配 malloc() free() realloc() new / delete // 标准I/O printf() fprintf() fopen() fclose() // Objective-C objc_msgSend() NSLog() 任何OC方法调用 // C++ 大多数C++标准库函数 异常处理 // 其他 syslog() strtok() localtime() 完整的崩溃信号处理器 实现示例 #include <signal.h> #include <execinfo.h> #include <unistd.h> #include <fcntl.h> #include <string.h> // 需要捕获的信号 static const int kFatalSignals[] = { SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGTRAP, SIGSYS, }; static const int kFatalSignalsCount = sizeof(kFatalSignals) / sizeof(kFatalSignals[0]); // 原始信号处理器 static struct sigaction g_originalActions[32]; // 崩溃日志文件路径(预先设置) static char g_crashLogPath[1024]; // 防止重入的标志 static volatile sig_atomic_t g_handling = 0; // 异步安全的字符串写入 static void safe_write_string(int fd, const char *str) { if (str) { write(fd, str, strlen(str)); } } // 异步安全的整数转字符串 static void safe_write_int(int fd, long value) { char buffer[32]; char *ptr = buffer + sizeof(buffer) - 1; *ptr = '\0'; int negative = value < 0; if (negative) value = -value; do { *--ptr = '0' + (value % 10); value /= 10; } while (value > 0); if (negative) *--ptr = '-'; write(fd, ptr, buffer + sizeof(buffer) - 1 - ptr); } // 异步安全的十六进制写入 static void safe_write_hex(int fd, unsigned long value) { char buffer[32]; char *ptr = buffer + sizeof(buffer) - 1; *ptr = '\0'; static const char hex[] = "0123456789abcdef"; do { *--ptr = hex[value & 0xf]; value >>= 4; } while (value > 0); *--ptr = 'x'; *--ptr = '0'; write(fd, ptr, buffer + sizeof(buffer) - 1 - ptr); } // 信号处理函数 static void crash_signal_handler(int sig, siginfo_t *info, void *context) { // 防止重入 if (g_handling) { return; } g_handling = 1; // 打开崩溃日志文件 int fd = open(g_crashLogPath, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { // 无法打开文件,尝试写入stderr fd = STDERR_FILENO; } // 写入基本信息 safe_write_string(fd, "=== Crash Report ===\n"); safe_write_string(fd, "Signal: "); safe_write_int(fd, sig); safe_write_string(fd, " ("); // 信号名称 switch (sig) { case SIGABRT: safe_write_string(fd, "SIGABRT"); break; case SIGBUS: safe_write_string(fd, "SIGBUS"); break; case SIGFPE: safe_write_string(fd, "SIGFPE"); break; case SIGILL: safe_write_string(fd, "SIGILL"); break; case SIGSEGV: safe_write_string(fd, "SIGSEGV"); break; case SIGTRAP: safe_write_string(fd, "SIGTRAP"); break; default: safe_write_string(fd, "UNKNOWN"); break; } safe_write_string(fd, ")\n"); // 信号代码 safe_write_string(fd, "Code: "); safe_write_int(fd, info->si_code); safe_write_string(fd, "\n"); // 故障地址 if (sig == SIGSEGV || sig == SIGBUS) { safe_write_string(fd, "Fault Address: "); safe_write_hex(fd, (unsigned long)info->si_addr); safe_write_string(fd, "\n"); } // 获取堆栈 // 注意:backtrace()严格来说也不是异步信号安全的函数 // 但为了获取崩溃堆栈,这是一个常见的权衡取舍 safe_write_string(fd, "\nBacktrace:\n"); void *callstack[128]; int frames = backtrace(callstack, 128); // 注意:backtrace_symbols使用malloc,不是异步安全的 // 这里我们只写入地址,符号化在后续处理 for (int i = 0; i < frames; i++) { safe_write_int(fd, i); safe_write_string(fd, ": "); safe_write_hex(fd, (unsigned long)callstack[i]); safe_write_string(fd, "\n"); } // 同步写入 if (fd != STDERR_FILENO) { fsync(fd); close(fd); } // 恢复原始处理器并重新发送信号 sigaction(sig, &g_originalActions[sig], NULL); raise(sig); } // 安装信号处理器 void install_crash_signal_handlers(const char *logPath) { // 保存日志路径 strncpy(g_crashLogPath, logPath, sizeof(g_crashLogPath) - 1); // 设置备用栈 static char alternate_stack[SIGSTKSZ]; stack_t ss; ss.ss_sp = alternate_stack; ss.ss_size = SIGSTKSZ; ss.ss_flags = 0; sigaltstack(&ss, NULL); // 配置信号处理器 struct sigaction action; memset(&action, 0, sizeof(action)); action.sa_sigaction = crash_signal_handler; action.sa_flags = SA_SIGINFO | SA_ONSTACK; sigemptyset(&action.sa_mask); // 注册所有致命信号 for (int i = 0; i < kFatalSignalsCount; i++) { int sig = kFatalSignals[i]; sigaction(sig, &action, &g_originalActions[sig]); } } // 卸载信号处理器 void uninstall_crash_signal_handlers(void) { for (int i = 0; i < kFatalSignalsCount; i++) { int sig = kFatalSignals[i]; sigaction(sig, &g_originalActions[sig], NULL); } } 获取上下文信息 从ucontext获取寄存器 #include <signal.h> #include <sys/ucontext.h> void signal_handler(int sig, siginfo_t *info, void *context) { ucontext_t *uc = (ucontext_t *)context; #if defined(__arm64__) || defined(__aarch64__) // ARM64架构 mcontext_t mc = uc->uc_mcontext; // 通用寄存器 for (int i = 0; i < 29; i++) { printf("x%d: 0x%llx\n", i, mc->__ss.__x[i]); } // 特殊寄存器 printf("fp: 0x%llx\n", mc->__ss.__fp); // 帧指针 printf("lr: 0x%llx\n", mc->__ss.__lr); // 链接寄存器 printf("sp: 0x%llx\n", mc->__ss.__sp); // 栈指针 printf("pc: 0x%llx\n", mc->__ss.__pc); // 程序计数器 printf("cpsr: 0x%x\n", mc->__ss.__cpsr); // 状态寄存器 #elif defined(__x86_64__) // x86_64架构 mcontext_t mc = uc->uc_mcontext; printf("rax: 0x%llx\n", mc->__ss.__rax); printf("rbx: 0x%llx\n", mc->__ss.__rbx); printf("rcx: 0x%llx\n", mc->__ss.__rcx); printf("rdx: 0x%llx\n", mc->__ss.__rdx); printf("rsi: 0x%llx\n", mc->__ss.__rsi); printf("rdi: 0x%llx\n", mc->__ss.__rdi); printf("rbp: 0x%llx\n", mc->__ss.__rbp); printf("rsp: 0x%llx\n", mc->__ss.__rsp); printf("rip: 0x%llx\n", mc->__ss.__rip); #endif } 从寄存器回溯堆栈 // 基于帧指针的堆栈回溯 void backtrace_from_context(ucontext_t *context) { mcontext_t mc = context->uc_mcontext; #if defined(__arm64__) uintptr_t pc = mc->__ss.__pc; uintptr_t fp = mc->__ss.__fp; printf("Backtrace:\n"); printf("0: 0x%lx (PC)\n", (unsigned long)pc); int frame = 1; while (fp != 0 && frame < 128) { // ARM64栈帧布局: // [fp+0]: 上一帧的fp // [fp+8]: 返回地址 uintptr_t *frame_ptr = (uintptr_t *)fp; // 安全检查 if ((uintptr_t)frame_ptr < 0x1000) break; uintptr_t return_addr = frame_ptr[1]; printf("%d: 0x%lx\n", frame, (unsigned long)return_addr); // 移动到上一帧 fp = frame_ptr[0]; frame++; } #endif } 信号掩码 阻塞信号 #include <signal.h> // 阻塞特定信号 void block_signals(void) { sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTERM); // 阻塞信号 sigprocmask(SIG_BLOCK, &mask, NULL); // 执行关键代码... // 解除阻塞 sigprocmask(SIG_UNBLOCK, &mask, NULL); } // 在信号处理期间阻塞其他信号 void setup_handler_with_mask(void) { struct sigaction action; action.sa_sigaction = signal_handler; action.sa_flags = SA_SIGINFO; // 在处理SIGSEGV时阻塞其他信号 sigfillset(&action.sa_mask); sigaction(SIGSEGV, &action, NULL); } 检查待处理信号 // 检查是否有信号待处理 void check_pending_signals(void) { sigset_t pending; sigpending(&pending); if (sigismember(&pending, SIGINT)) { printf("SIGINT is pending\n"); } } 信号与多线程 线程与信号 多线程环境下的信号处理: 1. 信号处理器是进程级的 • 所有线程共享信号处理器设置 • 一个线程设置的处理器影响整个进程 2. 信号掩码是线程级的 • 每个线程有自己的信号掩码 • pthread_sigmask()用于设置线程的信号掩码 3. 信号投递 • 同步信号(如SIGSEGV)投递给触发它的线程 • 异步信号投递给任意一个不阻塞该信号的线程 线程信号掩码 #include <pthread.h> #include <signal.h> // 设置线程信号掩码 void setup_thread_signal_mask(void) { sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGPIPE); // 阻塞SIGPIPE pthread_sigmask(SIG_BLOCK, &mask, NULL); } // 专门的信号处理线程 void *signal_handler_thread(void *arg) { sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGTERM); sigaddset(&mask, SIGINT); int sig; while (1) { // 同步等待信号 sigwait(&mask, &sig); printf("Received signal: %d\n", sig); if (sig == SIGTERM) { // 处理终止信号 break; } } return NULL; } // 主线程设置 void setup_signal_thread(void) { // 在主线程中阻塞这些信号 sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGTERM); sigaddset(&mask, SIGINT); pthread_sigmask(SIG_BLOCK, &mask, NULL); // 创建信号处理线程 pthread_t thread; pthread_create(&thread, NULL, signal_handler_thread, NULL); } 常见问题与解决方案 信号处理器中的死锁 问题:信号处理器中调用了非异步安全函数导致死锁 场景: 1. 主线程调用malloc(),获取了内存分配锁 2. 此时收到SIGSEGV信号 3. 信号处理器中调用malloc() 4. 尝试获取同一把锁 -> 死锁 解决方案: • 只使用异步信号安全的函数 • 使用预分配的内存 • 将复杂处理延迟到信号处理器外部 信号处理器重入 // 问题:信号处理器被再次调用 // 解决:使用标志位防止重入 static volatile sig_atomic_t g_handling = 0; void signal_handler(int sig, siginfo_t *info, void *context) { // 检查是否正在处理 if (g_handling) { // 已经在处理中,直接返回 return; } // 设置标志 g_handling = 1; // 处理信号... // 注意:不要在这里清除标志 // 因为我们会重新发送信号 } 与其他SDK的冲突 // 问题:多个SDK都注册了信号处理器 // 解决方案:保存并调用原始处理器 static struct sigaction g_original_action; void my_signal_handler(int sig, siginfo_t *info, void *context) { // 我的处理逻辑 handle_crash(sig, info, context); // 调用原始处理器 if (g_original_action.sa_flags & SA_SIGINFO) { g_original_action.sa_sigaction(sig, info, context); } else if (g_original_action.sa_handler != SIG_DFL && g_original_action.sa_handler != SIG_IGN) { g_original_action.sa_handler(sig); } else { // 恢复默认行为 signal(sig, SIG_DFL); raise(sig); } } void install_handler(void) { struct sigaction action; action.sa_sigaction = my_signal_handler; action.sa_flags = SA_SIGINFO | SA_ONSTACK; sigemptyset(&action.sa_mask); // 保存原始处理器 sigaction(SIGSEGV, &action, &g_original_action); } 信号与Mach异常的关系 转换关系 flowchart LR subgraph Mach["Mach异常"] A1[EXC_BAD_ACCESS] A2[EXC_BAD_INSTRUCTION] A3[EXC_ARITHMETIC] A4[EXC_BREAKPOINT] A5[EXC_CRASH] A6[EXC_SOFTWARE] end subgraph Signal["Unix信号"] B1[SIGSEGV / SIGBUS] B2[SIGILL] B3[SIGFPE] B4[SIGTRAP] B5[SIGABRT] B6[取决于具体代码] end A1 --> B1 A2 --> B2 A3 --> B3 A4 --> B4 A5 --> B5 A6 --> B6 转换时机: ...

May 2, 2026

崩溃-原理

理解崩溃的底层原理是有效治理的基础。本文详细介绍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_ACCESS SIGSEGV/SIGBUS 访问无效内存 EXC_BAD_INSTRUCTION SIGILL 非法指令 EXC_ARITHMETIC SIGFPE 算术异常(如除零) EXC_BREAKPOINT SIGTRAP 断点/调试陷阱 EXC_CRASH SIGABRT 程序主动abort Mach异常机制 Mach内核简介 Mach是macOS/iOS的微内核,提供了最基础的系统服务: ...

May 10, 2026

+load与+initialize的区别

+load和+initialize是Objective-C中两个特殊的类方法,它们都会被Runtime自动调用,但调用时机、调用方式和使用场景有很大区别。理解它们的差异对于iOS开发和性能优化非常重要。 基本定义 +load方法 +load方法在类被加载到内存时由Runtime自动调用,发生在main函数执行之前。 @implementation MyClass + (void)load { NSLog(@"MyClass loaded"); } @end +initialize方法 +initialize方法在类第一次收到消息时由Runtime自动调用,是一种懒加载机制。 @implementation MyClass + (void)initialize { NSLog(@"MyClass initialized"); } @end 核心区别对比 特性 +load +initialize 调用时机 main函数之前,类加载时 类首次收到消息时 调用方式 直接调用函数指针 通过objc_msgSend 调用次数 每个类只调用一次 可能被调用多次 是否阻塞启动 是 否 是否需要显式调用父类 否,自动调用 否,自动调用 Category的行为 都会被调用 会覆盖主类实现 线程安全 是 是 调用顺序 父类 -> 子类 -> Category 父类 -> 子类 未实现时的行为 不调用 可能调用父类实现 调用时机详解 +load的调用时机 +load的调用发生在dyld加载镜像的过程中: dyld加载Mach-O ↓ 映射到内存 ↓ 读取__DATA段中的__objc_nlclslist(非懒加载类列表) ↓ 调用所有类的+load方法 ↓ 读取__objc_nlcatlist(非懒加载分类列表) ↓ 调用所有分类的+load方法 ↓ main()函数执行 +initialize的调用时机 +initialize在类第一次收到消息时调用: ...

May 2, 2026

mmap详解

mmap(Memory-mapped file,内存映射文件)是一种将文件或设备映射到进程地址空间的技术。通过mmap,可以像操作内存一样操作文件,是iOS开发中实现高性能IO的重要手段。 基本概念 什么是mmap mmap是一个POSIX系统调用,它在进程的虚拟地址空间中创建一个映射,将文件内容映射到内存地址。映射建立后,对该内存区域的读写操作会直接反映到文件上。 文件映射:将磁盘上的普通文件映射到内存,用于高效读写文件内容(iOS开发中最常用) 设备映射:将硬件设备的I/O内存(如显卡显存、网卡缓冲区)映射到用户空间,让程序可以像读写内存一样直接与硬件交互,常用于驱动开发和嵌入式系统 flowchart LR subgraph traditional["传统文件IO (read/write)"] direction LR U1["用户缓冲区"] <-- "② 数据拷贝" --> K1["内核缓冲区"] K1 <-- "① 磁盘读取" --> F1["磁盘文件"] end subgraph mmap_io["mmap 内存映射"] direction LR U2["虚拟地址空间映射区域"] <-. "直接映射零拷贝" .-> F2["磁盘文件"] end traditional ~~~ mmap_io 方式 数据流向 拷贝次数 传统IO 磁盘 → 内核缓冲 → 用户缓冲 2次 mmap 磁盘 ↔ 映射区域(直接访问) 0次 mmap的工作原理 flowchart TD A["调用mmap()"] --> B["内核在进程虚拟地址空间分配一段连续地址"] B --> C["建立虚拟地址到文件的映射关系(页表项指向文件页)"] C --> D["返回映射区域的起始地址"] D --> E["进程访问映射地址"] E --> F{"页面是否在内存中?"} F -- 否 --> G["触发缺页中断(Page Fault)"] G --> H["内核从磁盘读取对应页面到物理内存"] H --> I["更新页表,建立映射"] I --> J["进程继续访问"] F -- 是 --> J J --> K["读写操作直接作用于内存"] K --> L["内核适时将脏页写回磁盘"] API详解 mmap函数原型 #include <sys/mman.h> void *mmap( void *addr, // 建议的映射起始地址,通常传NULL让系统决定 size_t length, // 映射区域的长度(字节) int prot, // 内存保护标志(可读/可写/可执行) int flags, // 映射类型标志 int fd, // 文件描述符 off_t offset // 文件偏移量(必须是页大小的整数倍) ); // 返回值:成功返回映射区域的起始地址,失败返回MAP_FAILED 参数详解 prot(内存保护标志): ...

May 7, 2026

崩溃-治理

本文详细介绍常见崩溃类型的修复方法、防崩溃保护机制以及线上监控体系。 崩溃治理是 APM 稳定性子系的核心模块。完整的监控建设、多类型稳定性问题(Watchdog/OOM/CPU/IO)的统一治理与 Zombie/Coredump/MemoryGraph 等高级归因能力,请参考 APM 系列:指标体系、业界方案。 崩溃治理流程 flowchart TB subgraph S1[1. 崩溃发现] A1[线上监控报警] A2[用户反馈] A3[测试发现] end subgraph S2[2. 崩溃分析] B1[符号化堆栈] B2[问题定位] B3[根因分析] end subgraph S3[3. 问题修复] C1[代码修复] C2[防护措施] C3[回归测试] end subgraph S4[4. 效果验证] D1[灰度发布] D2[监控崩溃率] D3[持续跟踪] end A1 --> S2 A2 --> S2 A3 --> S2 S2 --> S3 S3 --> S4 常见崩溃类型及修复 1. 数组越界 // 崩溃示例 NSArray *array = @[@1, @2, @3]; id obj = array[10]; // NSRangeException // 修复方案1:边界检查 - (id)safeObjectAtIndex:(NSUInteger)index fromArray:(NSArray *)array { if (index < array.count) { return array[index]; } return nil; } // 修复方案2:分类安全方法 @implementation NSArray (Safe) - (id)safeObjectAtIndex:(NSUInteger)index { if (index < self.count) { return [self objectAtIndex:index]; } return nil; } @end // 修复方案3:Swift可选绑定 extension Array { subscript(safe index: Index) -> Element? { return indices.contains(index) ? self[index] : nil } } // 使用 let value = array[safe: 10] // 返回nil而不是崩溃 2. 字典插入nil // 崩溃示例 NSMutableDictionary *dict = [NSMutableDictionary dictionary]; NSString *value = nil; [dict setObject:value forKey:@"key"]; // NSInvalidArgumentException // 修复方案1:nil检查 - (void)setObject:(id)object forKey:(id)key inDict:(NSMutableDictionary *)dict { if (object && key) { dict[key] = object; } } // 修复方案2:分类安全方法 @implementation NSMutableDictionary (Safe) - (void)safeSetObject:(id)object forKey:(id)key { if (object && key) { [self setObject:object forKey:key]; } } @end // 修复方案3:使用NSNull dict[@"key"] = value ?: [NSNull null]; 3. Unrecognized Selector // 崩溃示例 NSString *str = @"hello"; [str performSelector:@selector(count)]; // unrecognized selector // 修复方案1:respondsToSelector检查 if ([obj respondsToSelector:@selector(someMethod)]) { [obj performSelector:@selector(someMethod)]; } // 修复方案2:消息转发防护 @implementation NSObject (CrashProtection) + (void)load { // Hook forwardingTargetForSelector: Method original = class_getInstanceMethod(self, @selector(forwardingTargetForSelector:)); Method swizzled = class_getInstanceMethod(self, @selector(safe_forwardingTargetForSelector:)); method_exchangeImplementations(original, swizzled); } - (id)safe_forwardingTargetForSelector:(SEL)aSelector { // 如果找不到方法,返回一个空实现的对象 id target = [self safe_forwardingTargetForSelector:aSelector]; if (target) return target; // 记录错误 NSLog(@"Unrecognized selector: %@ on %@", NSStringFromSelector(aSelector), self); // 返回一个能处理任何消息的对象 return [CrashStub shared]; } @end 4. KVO崩溃 // 崩溃场景1:重复移除观察者 [obj removeObserver:self forKeyPath:@"value"]; [obj removeObserver:self forKeyPath:@"value"]; // 崩溃 // 崩溃场景2:未移除观察者就释放 - (void)dealloc { // 忘记移除观察者 } // 修复方案:安全的KVO封装 @interface SafeKVOProxy : NSObject @property (nonatomic, weak) id observer; @property (nonatomic, weak) id target; @property (nonatomic, copy) NSString *keyPath; @property (nonatomic, copy) void (^callback)(id newValue); @end @implementation SafeKVOProxy - (instancetype)initWithTarget:(id)target keyPath:(NSString *)keyPath observer:(id)observer callback:(void (^)(id))callback { self = [super init]; if (self) { _target = target; _keyPath = keyPath; _observer = observer; _callback = callback; [target addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL]; } return self; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (self.callback) { self.callback(change[NSKeyValueChangeNewKey]); } } - (void)dealloc { [_target removeObserver:self forKeyPath:_keyPath]; } @end 5. 多线程崩溃 // 崩溃场景:非线程安全的集合操作 // 线程1 [mutableArray addObject:obj1]; // 线程2 [mutableArray removeObjectAtIndex:0]; // 可能崩溃 // 修复方案1:加锁 @property (nonatomic, strong) NSLock *arrayLock; - (void)safeAddObject:(id)object { [self.arrayLock lock]; [self.mutableArray addObject:object]; [self.arrayLock unlock]; } // 修复方案2:使用GCD串行队列 @property (nonatomic, strong) dispatch_queue_t arrayQueue; - (void)safeAddObject:(id)object { dispatch_sync(self.arrayQueue, ^{ [self.mutableArray addObject:object]; }); } // 修复方案3:使用Actor(Swift Concurrency) actor SafeStorage { private var items: [Any] = [] func append(_ item: Any) { items.append(item) } func remove(at index: Int) { guard index < items.count else { return } items.remove(at: index) } func getAll() -> [Any] { return items } } let storage = SafeStorage() await storage.append(obj1) 6. 野指针崩溃 // 崩溃场景 __unsafe_unretained id unsafeRef = obj; obj = nil; [unsafeRef description]; // EXC_BAD_ACCESS // 修复方案1:使用weak而非unsafe_unretained __weak id weakRef = obj; obj = nil; [weakRef description]; // 安全,weakRef为nil // 修复方案2:Zombie Objects检测(调试用) // 在Scheme中启用Zombie Objects // 修复方案3:野指针检测工具 // 使用Address Sanitizer // Product -> Scheme -> Edit Scheme -> Diagnostics -> Address Sanitizer 7. Swift强制解包崩溃 // 崩溃示例 let value: String? = nil print(value!) // Fatal error: Unexpectedly found nil // 修复方案1:可选绑定 if let value = optionalValue { print(value) } // 修复方案2:空合运算符 let value = optionalValue ?? "default" // 修复方案3:guard语句 guard let value = optionalValue else { return } print(value) // 修复方案4:可选链 let length = optionalString?.count // 返回Int? 8. OOM(Out Of Memory)崩溃 OOM 是 iOS 最难治理的崩溃类型之一:系统因进程占用内存超过限制而将其强杀,不会产生标准崩溃日志,App 也没有机会在崩溃现场同步写下详细堆栈。因此 OOM 治理必须在事故前就把现场数据预埋下来,分为三个时段: ...

May 2, 2026

崩溃-采集

准确采集崩溃信息是分析和修复问题的前提。本文详细介绍iOS崩溃的捕获方案、堆栈回溯技术以及符号化原理。 崩溃采集架构 flowchart TB subgraph capture["崩溃捕获层"] direction LR mach["Mach Exception Handler"] signal["Unix Signal Handler"] exception["NSException Handler"] end subgraph collect["信息采集层"] direction LR backtrace["堆栈回溯(Backtrace)"] register["寄存器状态"] thread["线程信息"] device["设备/应用信息"] end subgraph storage["本地存储层"] direction LR async_write["异步安全写入"] file_manage["崩溃文件管理"] end subgraph report["上报处理层"] direction LR symbolicate["符号化解析"] aggregate["崩溃聚合"] analyze["数据分析"] end capture --> collect collect --> storage storage --> report Mach异常捕获 Mach是macOS/iOS的内核,提供了底层的异常处理机制。Mach异常是最底层的异常类型,发生在内核态,比Unix信号更早被触发。通过注册Mach异常处理器,可以在信号处理之前捕获到崩溃信息。 核心概念: Mach Port(端口):Mach内核中进程间通信(IPC)的基本单元,类似于文件描述符 Exception Port(异常端口):专门用于接收异常消息的端口 Task:Mach中的任务概念,对应一个进程,包含地址空间和资源 异常端口注册 #include <mach/mach.h> // 异常处理线程函数 static void *exception_handler_thread(void *arg) { mach_port_t exception_port = (mach_port_t)(uintptr_t)arg; while (1) { // 接收异常消息 struct { mach_msg_header_t head; mach_msg_body_t body; // ... 其他字段 } request; mach_msg_return_t result = mach_msg( &request.head, MACH_RCV_MSG | MACH_RCV_LARGE, 0, sizeof(request), exception_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL ); if (result == MACH_MSG_SUCCESS) { // 处理异常 handle_exception(&request); } } return NULL; } // 注册异常端口 kern_return_t register_exception_handler(void) { kern_return_t kr; mach_port_t exception_port; // 创建异常端口 kr = mach_port_allocate( mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &exception_port ); if (kr != KERN_SUCCESS) return kr; // 添加发送权限 kr = mach_port_insert_right( mach_task_self(), exception_port, exception_port, MACH_MSG_TYPE_MAKE_SEND ); if (kr != KERN_SUCCESS) return kr; // 设置任务异常端口 kr = task_set_exception_ports( mach_task_self(), EXC_MASK_BAD_ACCESS | EXC_MASK_BAD_INSTRUCTION | EXC_MASK_ARITHMETIC | EXC_MASK_BREAKPOINT, exception_port, EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE ); if (kr != KERN_SUCCESS) return kr; // 创建异常处理线程 pthread_t thread; pthread_create(&thread, NULL, exception_handler_thread, (void *)(uintptr_t)exception_port); return KERN_SUCCESS; } Mach异常处理流程 flowchart TD A["1. 异常发生"] --> B["2. 内核发送异常消息到异常端口"] B --> C["3. 异常处理线程接收消息"] C --> D["4. 解析异常信息"] D --> D1["exception type"] D --> D2["exception code"] D --> D3["thread state"] D1 & D2 & D3 --> E["5. 采集崩溃信息"] E --> E1["堆栈回溯"] E --> E2["寄存器状态"] E --> E3["其他上下文"] E1 & E2 & E3 --> F["6. 保存崩溃日志"] F --> G["7. 决定后续处理"] G --> G1["转发给原异常处理器"] G --> G2["让进程终止"] Unix信号捕获 Unix信号(Signal)是操作系统向进程发送的异步通知机制。当发生特定事件(如非法内存访问、算术错误等)时,内核会向进程发送相应的信号。iOS中,Mach异常通常会被转换为对应的Unix信号。 ...

June 21, 2026

RunLoop

RunLoop 是 iOS/macOS 中用于管理线程的事件循环机制。它的核心作用是让线程在有任务时处理任务,没有任务时进入休眠状态,从而避免线程退出并节省 CPU 资源。 简单来说,RunLoop 就是一个 do-while 循环: // 伪代码 void CFRunLoopRun() { do { // 处理各种事件源 // 如果没有事件,线程休眠 // 有事件时被唤醒处理 } while (running); } RunLoop 与线程的关系 每个线程都有唯一对应的 RunLoop 对象 主线程和子线程的 RunLoop 都采用懒加载机制,在第一次获取时才创建 主线程的 RunLoop 由 UIApplicationMain 内部首次获取并启动(调用链:UIApplicationMain → GSEventRunModal → CFRunLoopRunSpecific) RunLoop 与线程是一一对应的,存储在一个全局字典(__CFRunLoops)中 // 获取当前线程的 RunLoop CFRunLoopRef runLoop = CFRunLoopGetCurrent(); NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; // 获取主线程的 RunLoop CFRunLoopRef mainRunLoop = CFRunLoopGetMain(); NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop]; RunLoop 的核心组成 RunLoop 包含以下几个核心概念: 1. RunLoop 对象(CFRunLoopRef) RunLoop 对象本身,管理整个事件循环。在代码层面对应 CFRunLoopRef 类型。 ...

May 27, 2026

崩溃

崩溃是影响App稳定性的最严重问题。当应用发生崩溃时,用户会被强制退出应用,严重影响用户体验和产品口碑。 本系列文章系统性地介绍iOS崩溃的原理、采集方案以及治理策略。 什么是崩溃 崩溃(Crash)是指应用程序因为异常或错误而被强制终止的现象。从技术角度看,崩溃可以分为以下几类: 崩溃类型 触发原因 典型场景 Mach异常 底层硬件/内核异常 访问无效内存、除零错误 Unix信号 系统信号导致进程终止 SIGABRT、SIGSEGV、SIGBUS NSException OC层未捕获的异常 数组越界、unrecognized selector 内存问题 OOM或内存损坏 内存不足、野指针 Watchdog 系统监控超时 启动超时、后台任务超时 崩溃率指标 崩溃率是衡量App稳定性的核心指标: 崩溃率计算方式: 1. 崩溃用户率(推荐) 崩溃用户率 = 发生崩溃的用户数 / 总活跃用户数 × 100% 2. 崩溃次数率 崩溃次数率 = 崩溃次数 / 启动次数 × 100% 3. 崩溃Session率 崩溃Session率 = 崩溃Session数 / 总Session数 × 100% 崩溃处理的整体架构 ┌─────────────────────────────────────────────────────────────┐ │ 崩溃处理架构 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 崩溃发生 │ │ │ │ Mach异常 / Unix信号 / NSException / OOM │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 崩溃捕获 │ │ │ │ • Mach异常处理 │ │ │ │ • Signal Handler │ │ │ │ • NSSetUncaughtExceptionHandler │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 信息采集 │ │ │ │ • 堆栈信息 │ │ │ │ • 设备信息 │ │ │ │ • 用户上下文 │ │ │ │ • 崩溃现场 │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 本地存储 │ │ │ │ • 写入文件(需要异步安全) │ │ │ │ • 避免使用OC/Swift API │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 上报分析 │ │ │ │ • 下次启动时上报 │ │ │ │ • 符号化解析 │ │ │ │ • 聚合分析 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ 文章导航 本系列包含以下文章,建议按顺序阅读: ...

May 4, 2026

Runtime

什么是 Runtime Runtime 是 Objective-C 的运行时系统,是一套底层的 C 语言 API。Objective-C 是一门动态语言,很多操作都是在运行时而非编译时决定的,这一切都依赖于 Runtime。 Runtime 提供的核心能力: 消息发送与转发 方法交换(Method Swizzling) 关联对象(Associated Objects) 动态创建类和对象 动态添加和修改方法、属性 关于对象、类、isa 指针等底层数据结构的详细介绍,请参考 Objective-C底层原理-NSObject。本文专注于 Runtime 的动态能力和实际应用。 消息发送机制 Objective-C 的消息发送机制是其动态性的核心体现。与 Swift 支持静态派发不同,Objective-C 的方法调用都是动态消息派发。更多关于两者的对比,请参考 Objective-C与Swift区别。 objc_msgSend Objective-C 的方法调用本质上是消息发送,编译器会将方法调用转换为 objc_msgSend 函数: // 源代码 [obj doSomething]; // 编译后 objc_msgSend(obj, @selector(doSomething)); 消息发送流程 1. 检查 receiver 是否为 nil,如果是则直接返回 2. 通过 isa 找到 receiver 的类对象 3. 在类对象的方法缓存(cache_t)中查找方法 4. 如果缓存命中,直接调用方法实现(IMP) 5. 如果缓存未命中,在类对象的方法列表中查找 6. 如果找到,缓存方法并调用 7. 如果未找到,沿着 superclass 链向上查找 8. 如果最终未找到,进入消息转发流程 方法缓存 为了提高消息发送效率,Runtime 在类对象中使用哈希表缓存最近调用的方法: ...

May 2, 2026

static关键字详解

基础概念 static 是 C 语言定义的存储类说明符(Storage Class Specifier),Objective-C 和 Swift 中均有使用,但语义有所扩展。它的核心作用可以归纳为两点: 改变存储位置:将局部变量从栈区移到数据段,使其在函数返回后依然存活 限制链接可见性:将全局变量或函数的链接属性从 external 改为 internal,使其仅在当前编译单元(.m 文件)内可见 flowchart TD A["static 关键字"] --> B["修饰局部变量"] A --> C["修饰全局变量"] A --> D["修饰函数"] A --> E["Swift 中的 static"] B --> B1["存储位置:栈 → 数据段"] B --> B2["生命周期:函数作用域 → 整个进程"] B --> B3["作用域不变:仍限于函数内"] C --> C1["存储位置不变:仍在数据段"] C --> C2["链接属性:external → internal"] C --> C3["作用域缩小:全局 → 当前文件"] D --> D1["存储位置不变:仍在代码段"] D --> D2["链接属性:external → internal"] D --> D3["作用域缩小:全局 → 当前文件"] 一、静态局部变量 基本用法 在函数或方法内部用 static 修饰局部变量,使其只初始化一次,后续调用保留上次的值。 ...

May 3, 2026