崩溃-采集

准确采集崩溃信息是分析和修复问题的前提。本文详细介绍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

崩溃-原理

理解崩溃的底层原理是有效治理的基础。本文详细介绍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

启动优化-二进制重排

二进制重排是一种通过重新排列二进制文件中函数顺序来减少启动时Page Fault的优化技术。 基本原理 Page Fault问题 iOS使用虚拟内存管理,讨论Page Fault时需要先区分两层“内存”: 虚拟地址空间:进程看到的是一段连续的虚拟地址。App启动时,dyld会把Mach-O的__TEXT、__DATA等段映射到进程的虚拟地址空间里。这里的“映射”只是建立虚拟地址和文件偏移的关系,不代表整段二进制代码已经全部进入物理内存。 物理内存:CPU真正执行代码时,需要对应虚拟页背后有可用的物理页。代码页通常是文件映射页,第一次访问某个尚未驻留在物理内存中的代码页时,会触发Page Fault。内核再从App二进制文件中读取这个页的内容,填充到物理页,并更新页表。 所以,更准确地说:程序代码按页映射到虚拟地址空间;启动过程中实际执行到某个函数时,才会按需把该函数所在的代码页调入物理内存。Page Fault本身不是异常崩溃,而是虚拟内存按需调页的正常机制,只是冷启动时如果触发太多文件读取,会增加启动耗时。 默认情况下,编译器按照链接顺序排列函数,导致启动时调用的函数可能分散在不同的代码页中: 优化前(启动函数分散在不同代码页): ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ Page 1 │ │ Page 2 │ │ Page 3 │ │ Page 4 │ │ func_A │ │ func_X │ │ func_B │ │ func_C │ │ ... │ │ ... │ │ ... │ │ ... │ └────────┘ └────────┘ └────────┘ └────────┘ 启动调用 A→B→C 会触达 Page 1, 3, 4。 如果这些代码页还没有驻留在物理内存中,就可能产生3次Page Fault。 二进制重排的效果 通过重排,将启动时调用的函数集中排列在相邻的代码页中: ...

May 8, 2026

耗电

耗电(Energy Consumption)是移动应用质量的重要指标之一。一款耗电过快的App会直接影响用户的续航体验,轻则被用户吐槽,重则被系统标记为"耗电大户"(在iOS的"设置 → 电池"中显示),最终被用户卸载。 本系列文章从iOS功耗模型入手,系统性地介绍耗电问题的原理、检测、优化与治理。 为什么要关注耗电 iOS设备的电池容量有限(iPhone通常在3000~5000mAh),而应用的功耗会直接影响续航时间。苹果在iOS 8之后提供了"电池用量"统计入口,用户可以清楚地看到每个App消耗的电量比例。 常见的耗电投诉场景: 场景 用户感知 典型原因 后台待机一晚电量掉20% 手机放着不用也掉电 后台定位、后台音频、后台Fetch未合理释放 App使用1小时电量掉30% 使用过程电量下降过快 CPU长时间高占用、高频网络请求、GPU密集渲染 手机发烫 设备温度异常升高 CPU/GPU长期满载、持续定位、频繁屏幕唤醒 被系统标记为"后台活动"过高 系统提示"此App正在消耗大量电池" 后台任务未合理调度,违反能效规则 苹果对"耗电"非常敏感——从iOS 9开始引入低电量模式(Low Power Mode),从iOS 13开始加强后台任务的能效审查(BackgroundTasks框架),从iOS 15开始通过MetricKit将功耗数据开放给开发者。 iOS功耗模型概述 iOS设备的功耗来源可以分为以下几大类: flowchart TB Battery["电池总功耗"] Battery --> CPU["CPU计算、编解码"] Battery --> GPU["GPU渲染、图层合成"] Battery --> Screen["屏幕显示、亮度、刷新率"] Battery --> Network["网络蜂窝、WiFi、BLE"] Battery --> Location["定位GPS、基站、WiFi定位"] Battery --> Sensor["传感器加速度计、陀螺仪、气压计"] Battery --> Audio["音视频录制、播放、相机"] Battery --> Storage["存储磁盘IO"] Battery --> IO["外设蓝牙、NFC、UWB"] 不同硬件模块的功耗量级差异很大。参考苹果WWDC的数据,定位和蜂窝数据通常是"耗电大户",屏幕是"持续耗电大户",而CPU/GPU在计算密集时瞬时功耗最高。 模块 典型功耗等级 特点 屏幕 持续高 只要亮屏就持续耗电 蜂窝数据 高 搜索基站、维持连接都耗电 GPS定位 高 冷启动搜星耗电特别明显 CPU/GPU 中~高 计算量决定功耗 WiFi 中 比蜂窝省电 蓝牙(BLE) 低 低功耗蓝牙专为续航优化 传感器 低~中 高频采样会显著增加功耗 耗电问题的本质 耗电问题的本质可以用一个简单的公式表示: ...

May 8, 2026

耗电-检测

本文介绍iOS耗电问题的检测手段,从开发阶段的本地工具,到线上的监控体系,帮助团队形成一套完整的耗电可观测能力。 检测手段全景 flowchart TB subgraph Dev["研发阶段"] EG["Xcode Energy Gauge实时能效评分"] Inst["InstrumentsEnergy Log / Time Profiler"] XCT["XCTest能效测试"] end subgraph Test["测试阶段"] Thermal["热节流测试"] Battery["真机电量消耗测试"] IOKit["IOKit内部分析"] LPM["低电量模式回归"] end subgraph Online["线上阶段"] MK["MetricKit"] Self["自建实时采集电量 / CPU / Thermal / 生命周期"] Report["埋点上报数据看板"] end Dev --> Test --> Online 一、Xcode Energy Gauge Xcode在真机调试时,“Debug Navigator → Energy Impact"提供了实时能效评估。 能效评分(Energy Impact) Xcode将功耗分为5档: 等级 含义 典型值 Very Low / Low 正常使用 03 / 48 High 偏高,需要关注 9~16 Very High 严重,必须优化 17~20 评分由以下几个因素构成: CPU:平均CPU使用率。 Network:网络活跃时间与流量。 Location:定位活跃时间与精度。 GPU:GPU活跃时间。 Background:后台活跃时间。 Overhead:系统唤醒代价。 典型使用流程 Xcode连接真机(模拟器的数据不准确)。 运行App,选择Debug Navigator → Energy Impact。 进行典型操作(刷Feed、看视频、聊天等),观察评分。 如果出现High/Very High,使用Instruments进一步定位。 二、Instruments Energy Log Instruments中有多个和能效相关的工具: ...

May 8, 2026

卡顿-检测

准确检测卡顿是优化的前提。本文介绍多种卡顿检测方案,从开发调试到线上监控都有对应的方案。 检测方案概览 方案 原理 优点 缺点 适用场景 CADisplayLink 监控帧节奏、FPS、慢帧/掉帧 简单直接、适合趋势观察 无法直接给出堆栈,FPS 只能辅助判断 开发调试/线上辅助指标 RunLoop Observer 监控RunLoop状态 可获取堆栈 有一定开销 开发/线上 子线程Ping 定时检测主线程响应 实现简单 精度有限 线上监控 Instruments 系统级分析 信息详细 只能开发时用 性能分析 MetricKit 系统数据收集 无额外开销 iOS 13+ 线上监控 Sentry ANR V2 帧延迟分析 区分阻塞类型 需集成SDK 线上监控 1. CADisplayLink监控 基本原理 CADisplayLink 是一个和屏幕显示刷新节奏同步的回调。它适合回答“当前 UI 刷新是否顺畅、是否出现慢帧、掉帧或冻结帧”,但 FPS 本身只是辅助参考,不能单独作为卡顿结论。 原因是: FPS 只描述结果,不描述原因:FPS 下降只能说明某段时间显示帧变少了,不能告诉你是主线程计算、布局、图片解码、锁等待、I/O、GPU 渲染还是系统主动降刷新率导致的。 ProMotion 会动态调整刷新率:在支持 120Hz 的机型上,系统可能根据内容和功耗策略把刷新率降到 10Hz、24Hz、30Hz、60Hz、80Hz、120Hz 等。静止页面低 FPS 可能只是系统主动省电,不是卡顿。 主线程阻塞会让回调延迟:如果主线程真的被阻塞,CADisplayLink 回调本身也会延后,此时需要结合实际回调间隔、RunLoop 状态和堆栈采样判断。 因此线上卡顿监控更推荐记录 帧间隔 / 慢帧 / 冻结帧 / 交互场景,而不是只显示一个“当前 FPS”。 ...

May 7, 2026

耗电-治理

前面几篇分别讲了耗电的原理、检测、以及CPU/网络/定位/屏幕的具体优化手段。本文把这些内容整合起来,给出一套面向工程团队的 耗电治理体系:从研发规范、线上监控、问题排查到常态化运营。 耗电治理是 APM 资源子系的一部分,与稳定性、流畅性、启动指标共用同一套监控与告警基础设施。完整的 APM 建设方案见 APM 系列:数据采集、B端平台设计。 一、治理体系全景 一个成熟的iOS耗电治理体系通常由四个闭环组成: flowchart TB subgraph Dev["研发侧"] R1["能效规范"] R2["架构层能效约束"] R3["Code Review清单"] end subgraph Test["测试侧"] T1["XCTest能效基线"] T2["真机续航测试"] T3["极端场景矩阵"] end subgraph Online["线上侧"] O1["MetricKit接入"] O2["自建CPU/电量采样"] O3["场景化埋点"] O4["大盘 + 告警"] end subgraph Gov["运营侧"] G1["版本耗电报告"] G2["Top问题跟进"] G3["复盘与规范迭代"] end Dev --> Test --> Online --> Gov --> Dev 二、研发阶段的能效规范 模块准入清单 每个新增模块在接入主App前,必须回答以下问题: 维度 问题 CPU 是否有常驻Timer?是否有轮询?是否在不可见时暂停? 网络 请求频率?是否合并?是否支持后台取消? 定位 是否申请定位?精度多少?后台策略? 传感器 是否开启传感器?采样率?视图生命周期? 后台能力 是否申请Background Mode?如何收敛? 动画/渲染 是否有长驻动画?是否适配ProMotion? Low Power 低电量模式下的降级策略? 权限 涉及的用户权限?是否遵守最小权限原则? 架构层面的能效约束 一些通用能力可以在架构层统一管控,避免各业务重复踩坑: ...

May 7, 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

崩溃-Mach异常

本文深入介绍Mach异常的底层实现细节,包括端口操作、消息机制以及如何实现自定义的Mach异常处理器。 关于Mach异常的基础概念(Task、Thread、Port、Message)和异常端口的查找顺序,请参考 崩溃-原理。 Mach端口详解 端口权限 端口权限类型: ┌─────────────────────────────────────────────────────────────┐ │ 端口权限 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ MACH_PORT_RIGHT_SEND │ │ ├── 发送权限 │ │ ├── 可以向端口发送消息 │ │ └── 可以复制给其他任务 │ │ │ │ MACH_PORT_RIGHT_RECEIVE │ │ ├── 接收权限 │ │ ├── 可以从端口接收消息 │ │ ├── 只能有一个持有者 │ │ └── 持有者销毁时端口销毁 │ │ │ │ MACH_PORT_RIGHT_SEND_ONCE │ │ ├── 一次性发送权限 │ │ └── 发送一次后自动失效 │ │ │ │ MACH_PORT_RIGHT_PORT_SET │ │ ├── 端口集合权限 │ │ └── 可以同时监听多个端口 │ │ │ │ MACH_PORT_RIGHT_DEAD_NAME │ │ ├── 死名权限 │ │ └── 端口已销毁但名字仍存在 │ │ │ └─────────────────────────────────────────────────────────────┘ 端口操作 #include <mach/mach.h> // 分配新端口 mach_port_t port; kern_return_t kr = mach_port_allocate( mach_task_self(), // 当前任务 MACH_PORT_RIGHT_RECEIVE, // 接收权限 &port // 输出端口名 ); // 添加发送权限 kr = mach_port_insert_right( mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND // 创建发送权限 ); // 销毁端口 kr = mach_port_destroy(mach_task_self(), port); // 释放端口权限 kr = mach_port_deallocate(mach_task_self(), port); Mach消息 消息结构 // 消息头部 typedef struct { mach_msg_bits_t msgh_bits; // 消息类型和权限 mach_msg_size_t msgh_size; // 消息大小 mach_port_t msgh_remote_port; // 目标端口 mach_port_t msgh_local_port; // 本地端口(用于回复) mach_port_name_t msgh_voucher_port;// 凭证端口 mach_msg_id_t msgh_id; // 消息ID } mach_msg_header_t; // 完整消息示例 typedef struct { mach_msg_header_t header; // 可选的消息体 mach_msg_body_t body; // 可选的端口描述符 mach_msg_port_descriptor_t port; // 可选的数据 char data[256]; } MyMessage; 消息发送与接收 // 发送消息 kern_return_t send_message(mach_port_t port, const char *data, size_t len) { struct { mach_msg_header_t header; char data[256]; } message; message.header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0); message.header.msgh_size = sizeof(message); message.header.msgh_remote_port = port; message.header.msgh_local_port = MACH_PORT_NULL; message.header.msgh_id = 0; size_t copy_len = len < sizeof(message.data) ? len : sizeof(message.data); memcpy(message.data, data, copy_len); return mach_msg( &message.header, MACH_SEND_MSG, // 发送 sizeof(message), // 发送大小 0, // 接收大小 MACH_PORT_NULL, // 接收端口 MACH_MSG_TIMEOUT_NONE, // 超时 MACH_PORT_NULL // 通知端口 ); } // 接收消息 kern_return_t receive_message(mach_port_t port) { struct { mach_msg_header_t header; char data[256]; } message; kern_return_t kr = mach_msg( &message.header, MACH_RCV_MSG, // 接收 0, // 发送大小 sizeof(message), // 接收大小 port, // 接收端口 MACH_MSG_TIMEOUT_NONE, // 超时 MACH_PORT_NULL // 通知端口 ); if (kr == MACH_MSG_SUCCESS) { // 处理消息 printf("Received: %s\n", message.data); } return kr; } 异常端口操作 设置异常端口 #include <mach/mach.h> // 设置任务级异常端口 kern_return_t setup_task_exception_port(mach_port_t exception_port) { // 异常类型掩码 exception_mask_t mask = EXC_MASK_BAD_ACCESS | EXC_MASK_BAD_INSTRUCTION | EXC_MASK_ARITHMETIC | EXC_MASK_BREAKPOINT | EXC_MASK_SOFTWARE; return task_set_exception_ports( mach_task_self(), mask, exception_port, EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE ); } // 获取当前异常端口 kern_return_t get_exception_ports(void) { exception_mask_t masks[EXC_TYPES_COUNT]; mach_msg_type_number_t count = EXC_TYPES_COUNT; mach_port_t ports[EXC_TYPES_COUNT]; exception_behavior_t behaviors[EXC_TYPES_COUNT]; thread_state_flavor_t flavors[EXC_TYPES_COUNT]; return task_get_exception_ports( mach_task_self(), EXC_MASK_ALL, masks, &count, ports, behaviors, flavors ); } 异常处理流程 完整的异常处理流程 flowchart TD A["1. 硬件检测到异常CPU执行非法操作,触发硬件中断"] --> B["2. 陷入内核CPU切换到内核态,执行异常处理程序"] B --> C["3. 内核构造异常消息创建包含异常信息的Mach消息"] C --> D["4. 发送到异常端口按照 Thread → Task → Host 顺序查找端口"] D --> E["5. 等待回复内核暂停线程,等待异常处理程序的回复"] E --> F{"6. 处理回复"} F -->|KERN_SUCCESS| G["恢复线程执行"] F -->|其他| H["转换为Unix信号或终止进程"] 异常消息格式 // 异常消息结构 typedef struct { mach_msg_header_t Head; // 消息体 mach_msg_body_t msgh_body; mach_msg_port_descriptor_t thread; // 发生异常的线程 mach_msg_port_descriptor_t task; // 发生异常的任务 // 异常信息 NDR_record_t NDR; exception_type_t exception; // 异常类型 mach_msg_type_number_t codeCnt; // 代码数量 int64_t code[2]; // 异常代码 } __Request__mach_exception_raise_t; // 异常回复消息 typedef struct { mach_msg_header_t Head; NDR_record_t NDR; kern_return_t RetCode; // 返回码 } __Reply__mach_exception_raise_t; 实现Mach异常处理器 完整示例 #include <mach/mach.h> #include <pthread.h> // 异常处理线程 static pthread_t exception_thread; static mach_port_t exception_port; // 保存原始异常端口 static mach_port_t original_exception_port; static exception_behavior_t original_behavior; static thread_state_flavor_t original_flavor; // 异常处理函数 static kern_return_t handle_exception( mach_port_t exception_port, mach_port_t thread, mach_port_t task, exception_type_t exception, mach_exception_data_t code, mach_msg_type_number_t code_count ) { // 获取线程状态(ARM64架构) // 注意:如需支持x86_64模拟器,需要使用x86_thread_state64_t和X86_THREAD_STATE64 #if defined(__arm64__) arm_thread_state64_t state; mach_msg_type_number_t state_count = ARM_THREAD_STATE64_COUNT; thread_get_state(thread, ARM_THREAD_STATE64, (thread_state_t)&state, &state_count); // 记录崩溃信息 printf("Exception: %d\n", exception); printf("Code: 0x%llx, 0x%llx\n", code[0], code[1]); printf("PC: 0x%llx\n", (unsigned long long)state.__pc); printf("LR: 0x%llx\n", (unsigned long long)state.__lr); #endif // 采集堆栈(需要自行实现) // collect_backtrace(thread); // 保存崩溃日志(需要自行实现) // save_crash_log(); // 返回失败,让系统继续处理(转换为信号) return KERN_FAILURE; } // 异常处理线程主函数 static void *exception_handler_thread(void *arg) { kern_return_t kr; while (1) { // 接收异常消息 struct { mach_msg_header_t head; mach_msg_body_t body; mach_msg_port_descriptor_t thread; mach_msg_port_descriptor_t task; NDR_record_t NDR; exception_type_t exception; mach_msg_type_number_t code_count; int64_t code[2]; int flavor; mach_msg_type_number_t state_count; natural_t state[224]; } request; struct { mach_msg_header_t head; NDR_record_t NDR; kern_return_t ret_code; } reply; // 接收消息 kr = mach_msg( &request.head, MACH_RCV_MSG | MACH_RCV_LARGE, 0, sizeof(request), exception_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL ); if (kr != MACH_MSG_SUCCESS) { continue; } // 处理异常 kr = handle_exception( exception_port, request.thread.name, request.task.name, request.exception, request.code, request.code_count ); // 发送回复 reply.head.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MOVE_SEND_ONCE, 0); reply.head.msgh_size = sizeof(reply); reply.head.msgh_remote_port = request.head.msgh_remote_port; reply.head.msgh_local_port = MACH_PORT_NULL; // Mach IPC惯例:回复消息ID = 请求消息ID + 100 reply.head.msgh_id = request.head.msgh_id + 100; reply.NDR = NDR_record; reply.ret_code = kr; mach_msg( &reply.head, MACH_SEND_MSG, sizeof(reply), 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL ); } return NULL; } // 安装异常处理器 kern_return_t install_exception_handler(void) { kern_return_t kr; // 创建异常端口 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; // 保存原始异常端口 exception_mask_t mask = EXC_MASK_BAD_ACCESS | EXC_MASK_BAD_INSTRUCTION | EXC_MASK_ARITHMETIC | EXC_MASK_BREAKPOINT; // 用于接收查询结果的数组 exception_mask_t masks_out[EXC_TYPES_COUNT]; mach_port_t ports_out[EXC_TYPES_COUNT]; exception_behavior_t behaviors_out[EXC_TYPES_COUNT]; thread_state_flavor_t flavors_out[EXC_TYPES_COUNT]; mach_msg_type_number_t count = EXC_TYPES_COUNT; kr = task_get_exception_ports( mach_task_self(), mask, // 输入:要查询的异常类型掩码 masks_out, // 输出:实际的掩码数组 &count, // 输入输出:数组大小 ports_out, // 输出:端口数组 behaviors_out, // 输出:行为数组 flavors_out // 输出:flavor数组 ); // 保存第一个有效的原始端口(简化处理) if (count > 0) { original_exception_port = ports_out[0]; original_behavior = behaviors_out[0]; original_flavor = flavors_out[0]; } // 设置新的异常端口 kr = task_set_exception_ports( mach_task_self(), mask, exception_port, EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE ); if (kr != KERN_SUCCESS) return kr; // 创建异常处理线程 pthread_create(&exception_thread, NULL, exception_handler_thread, NULL); return KERN_SUCCESS; } 调试器检测 调试器(如LLDB)通过在Task(任务/进程)级别注册异常端口来优先拦截异常。了解这一机制有助于理解为什么调试时某些崩溃行为会有所不同。 ...

May 3, 2026

JetsamEvent 日志解读

JetsamEvent 是 iOS 唯一由系统官方生成、且专门记录"因内存原因终止进程"的日志。它不记录线程堆栈,却完整刻画了发生强杀那一刻整机的内存分布——这使它成为 OOM 归因、阈值建模、后台生命周期治理最权威的数据源。 本文聚焦 JetsamEvent 日志本身:存放位置、文件结构、字段含义、reason 类型、案例解读、自动化批量解析脚本,以及机型内存上限/iOS 版本差异等实战要点。OOM 监控治理的整体方法论见 崩溃-治理。 1. Jetsam 与 JetsamEvent 是什么 iOS 通过 XNU 内核的 memorystatus 子系统(俗称 Jetsam)做整机内存管理: 前台/后台进程按 Jetsam 优先级排序; 当系统整体可用物理页(vm_page_free_count)或单个进程 phys_footprint 触发阈值时,按优先级杀掉进程; 被杀时,内核在 /var/mobile/Library/Logs/CrashReporter/ 生成 JetsamEvent-yyyy-MM-dd-HHmmss.ips 文件,并通过 DiagnosticsReporting 暴露给 设置 → 隐私与安全性 → 分析与改进 → 分析数据。 需要理解两个关键结论: JetsamEvent 不是单个进程的崩溃日志,而是整机一次 Jetsam 事件的快照。 一个文件里往往包含几十个进程条目,只有其中一个(或少数)被真正杀掉;其余进程是作为"当时整机内存画像"被附带记录。 JetsamEvent 不含线程堆栈。 它的价值在于告诉你"谁占用了多少内存"和"为什么被杀",而不是"在哪一行代码被杀"。堆栈需要由 App 自己的 Memory Dump / MetricKit 补齐。 2. 获取方式 场景 路径 真机设置页查看 设置 → 隐私与安全性 → 分析与改进 → 分析数据,文件名 JetsamEvent-*.ips Xcode 连接真机 Window → Devices and Simulators → 选中设备 → View Device Logs sysdiagnose 包 解压后位于 crashes_and_spins/*.ips 越狱设备 /var/mobile/Library/Logs/CrashReporter/JetsamEvent-*.ips 自动上报 App 下次启动时扫描沙盒外的 Apple 日志目录不可行,只能依赖 MetricKit 的 MXCrashDiagnostic/MXAppExitMetric(iOS 14+) ⚠️ iOS 沙盒下 App 无法直接读取 /var/mobile/Library/Logs/ 的原始 ips 文件,线上规模化采集必须走 MetricKit。JetsamEvent 原始日志主要用于研发期抓真机、或用户反馈场景的手工分析。 ...

May 3, 2026