崩溃-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

包瘦身

包体积(IPA大小)是iOS应用的重要质量指标。App Store对蜂窝网络下载有200MB的提示阈值,超过这个大小会弹窗询问用户是否使用流量下载(iOS 13之前是强制要求Wi-Fi)。这个弹窗会增加用户的决策成本,影响下载转化率。因此,包体积优化对于用户获取和留存都有重要影响。 包格式介绍 iOS开发中常见的包格式有三种: 格式 扩展名 说明 .app 目录 应用程序包,是一个包含可执行文件和资源的目录结构 .xcarchive 目录 Xcode归档包,包含.app、dSYM符号文件、构建信息等 .ipa 文件 安装包,本质是ZIP压缩的.app,用于分发和安装 日常使用场景: 开发调试:Xcode直接将.app安装到模拟器或真机 测试分发:使用.ipa通过TestFlight、蒲公英、fir.im等平台分发给测试人员 App Store上架:通过Xcode或Transporter将.xcarchive导出并上传到App Store Connect(实际上传的是从xcarchive导出的IPA或直接上传xcarchive中的内容) .app(Application Bundle) .app是macOS/iOS应用的标准格式,实际上是一个目录(Finder中显示为单个文件)。包含: 可执行文件(Mach-O格式) Info.plist配置文件 资源文件(图片、音频、本地化文件等) Frameworks目录(动态库) PlugIns目录(App Extension) .xcarchive(Xcode Archive) .xcarchive是Xcode的归档格式,用于保存完整的构建产物。目录结构: YourApp.xcarchive/ ├── Products/ │ └── Applications/ │ └── YourApp.app # 应用程序包 ├── dSYMs/ │ └── YourApp.app.dSYM # 符号文件(用于崩溃解析) ├── Info.plist └── SCMBlueprint/ # 源码管理信息 主要用途: 保存dSYM符号文件,用于线上崩溃日志符号化 导出不同分发渠道的IPA(App Store、Ad Hoc、Enterprise) 上传到App Store Connect .ipa(iOS App Store Package) .ipa是iOS应用的安装包格式,本质是一个ZIP压缩文件。结构: ...

May 2, 2026

包瘦身-资源优化

资源文件通常占iOS应用包体积的40-60%,是优化的重要方向。本文介绍各种资源优化手段及其原理。 图片优化 Asset Catalog原理 Asset Catalog(.xcassets)编译后生成Assets.car文件,这是一个优化过的资源容器: 编译前: Images.xcassets/ ├── AppIcon.appiconset/ │ ├── icon-60@2x.png │ └── icon-60@3x.png ├── Logo.imageset/ │ ├── logo@2x.png │ └── logo@3x.png └── Contents.json 编译后: Assets.car ← 单一文件,包含所有资源 Assets.car的优化: 图片被重新编码,使用更高效的压缩 支持App Thinning,按设备分发 运行时高效加载(内存映射) 可以使用assetutil查看Assets.car内容: assetutil --info Assets.car 图片压缩原理 PNG压缩原理: PNG使用DEFLATE无损压缩,但可以通过以下方式减小体积: 原始PNG结构: ┌────────────────┐ │ PNG Header │ ├────────────────┤ │ IHDR (图像信息) │ ├────────────────┤ │ IDAT (图像数据) │ ← 压缩的像素数据 ├────────────────┤ │ 辅助chunks │ ← 可以移除 └────────────────┘ 优化方向: 1. 移除不必要的chunks(如tEXt、iTXt) 2. 优化DEFLATE压缩参数 3. 减少颜色数量(有损) 4. 使用更高效的滤波器 有损压缩原理(pngquant): ...

May 2, 2026

包瘦身-可执行文件优化

可执行文件通常占iOS应用包体积的30-50%,是优化的重点。本文介绍各种可执行文件优化手段及其原理。 编译器优化选项 优化级别原理 编译器优化级别控制了LLVM在编译时应用的优化Pass数量和类型。 什么是优化Pass? Pass是指编译器对代码进行的一次特定优化处理过程。LLVM将优化工作分解为多个独立的Pass,每个Pass负责一种特定类型的优化: 源代码 → 前端解析 → IR(中间表示)→ Pass1 → Pass2 → Pass3 → ... → 优化后的代码 → 机器码 常见的优化Pass包括: Pass名称 作用 Dead Code Elimination 删除永远不会执行的代码 Constant Folding 编译时计算常量表达式,如3+5直接变成8 Function Inlining 将函数调用替换为函数体本身 Loop Unrolling 展开循环,减少循环控制开销 Common Subexpression Elimination 消除重复计算的表达式 不同的优化级别启用不同数量和类型的Pass: # Build Settings Optimization Level = -Os # 级别:-Os 优化大小(推荐用于Release) -O0(无优化) 不应用任何优化Pass,编译速度最快。代码按原样生成,保留所有变量和控制流,便于调试。适用于Debug构建。 -O1(基础优化) 应用不显著增加编译时间的基础优化,包括: 基本的死代码消除 简单的常量折叠 基本块合并 死代码消除: 死代码消除(Dead Code Elimination)是指编译器自动移除永远不会被执行或执行结果不会被使用的代码。注意:这里的死代码消除是编译阶段的优化,作用于函数内部;而后文提到的Dead Code Stripping是链接阶段的优化,用于移除未被引用的整个函数或类。 -O1 适合需要一定优化但对编译速度敏感的场景。 -O2(标准优化) 应用大多数优化Pass,是性能和体积的平衡点。包括: ...

May 2, 2026

包瘦身-分析工具

在进行包体积优化之前,首先需要了解如何分析包体积。本文介绍常用的分析工具和Mach-O文件结构。 查看IPA构成 解压IPA文件分析各部分大小: # 解压IPA unzip -q YourApp.ipa -d ./ipa_content # 查看各文件大小 find ./ipa_content -type f -exec ls -lh {} \; | sort -k5 -h -r | head -20 LinkMap分析 LinkMap文件记录了链接器生成可执行文件的详细信息,可以精确分析二进制大小: # Build Settings中设置 Write Link Map File = YES Path to Link Map File = $(TARGET_TEMP_DIR)/$(PRODUCT_NAME)-LinkMap-$(CURRENT_VARIANT)-$(CURRENT_ARCH).txt LinkMap文件结构 # Path: /path/to/YourApp # Arch: arm64 # Object files: [ 0] linker synthesized [ 1] /path/to/ViewController.o [ 2] /path/to/Model.o # Sections: # Address Size Segment Section 0x100001000 0x00012345 __TEXT __text 0x100013345 0x00001234 __TEXT __stubs # Symbols: # Address Size File Name 0x100001000 0x00000100 [ 1] -[ViewController viewDidLoad] 0x100001100 0x00000050 [ 2] -[Model init] 解析脚本 可以使用脚本解析LinkMap统计各模块大小: ...

May 2, 2026

包瘦身-App Thinning

App Thinning是Apple提供的包体积优化技术,让用户只下载适合其设备的内容。本文详细介绍App Thinning的三个组成部分及其原理。 整体架构 开发者上传: ┌─────────────────────────────────────────┐ │ Universal IPA │ │ ├── arm64 代码 │ │ ├── @1x/@2x/@3x 图片 │ │ ├── iPhone/iPad 资源 │ │ └── Metal GPU资源(OpenGL ES已弃用) │ └─────────────────────────────────────────┘ ↓ App Store处理 ↓ ┌───────────┐ ┌──────────┐ ┌──────────┐ │ iPhone 12 │ │ iPhone 8 │ │ iPad Pro │ │ 变体 │ │ 变体 │ │ 变体 │ │ @3x图片 │ │ @2x图片 │ │ @2x图片 │ │ arm64 │ │ arm64 │ │ arm64 │ │ A14 GPU │ │ A11 GPU │ │ M1 GPU │ └───────────┘ └──────────┘ └──────────┘ App Thinning包含三个部分: ...

May 2, 2026

卡顿

卡顿是影响用户体验的关键问题之一。当应用无法在一帧的时间内(通常为16.67ms)完成渲染工作时,就会出现掉帧,用户会感知到界面不流畅。 本系列文章从渲染原理入手,系统性地介绍iOS卡顿的检测与优化方法。 卡顿监控是 APM 流畅性子系的核心能力,线上指标体系、检测采集、上报策略请参考 APM 系列:指标体系、数据采集、业界方案(微信 Matrix / 字节 Slardar 卡死监控)。 什么是卡顿 iOS设备的屏幕刷新率通常为60Hz(ProMotion设备可达120Hz),这意味着每秒需要渲染60帧画面。每一帧的渲染时间窗口约为16.67ms(1000ms / 60)。 当CPU或GPU无法在这个时间窗口内完成工作时,就会发生掉帧(Frame Drop),用户会感知到卡顿。 掉帧情况 用户感知 严重程度 偶尔掉1-2帧 几乎无感知 轻微 连续掉帧 明显卡顿 中等 长时间阻塞(>250ms) 界面冻结 严重 渲染流程概述 iOS的渲染流程涉及CPU和GPU的协作,通过Core Animation Pipeline完成: flowchart TB subgraph APP["App 进程(CPU 阶段)"] direction TB Layout["Layout(布局计算)layoutSubviews、约束计算"] Display["Display(绘制显示)drawRect:、文本绘制"] Prepare["Prepare(准备)图片解码、格式转换"] Commit["Commit(提交)打包图层树"] Layout --> Display --> Prepare --> Commit end subgraph RS["Render Server 进程(GPU 阶段)"] direction TB Decode["解码图层树"] Vertex["顶点着色器"] Raster["光栅化"] Fragment["片段着色器"] Buffer["帧缓冲区写入"] Decode --> Vertex --> Raster --> Fragment --> Buffer end APP -->|"IPC 传输"| RS RS --> VSync["VSync 信号到来"] VSync --> Screen["显示到屏幕"] CPU与GPU的职责 阶段 主要工作 常见瓶颈 CPU 布局计算、视图创建、文本计算、图片解码 复杂布局、大量文本、主线程阻塞 GPU 纹理渲染、图层混合、离屏渲染 离屏渲染、大量透明图层、超大图片 卡顿的常见原因 CPU瓶颈 主线程阻塞:耗时操作在主线程执行 复杂布局:大量约束计算、频繁布局 文本计算:复杂富文本、大量文本渲染 图片解码:大图片在主线程解码 对象创建:大量对象的创建和销毁 GPU瓶颈 离屏渲染:圆角、阴影、遮罩触发离屏渲染 图层混合:大量透明图层叠加 超大图片:纹理尺寸超过GPU限制 复杂特效:模糊、滤镜等GPU密集操作 文章导航 本系列包含以下文章,建议按顺序阅读: ...

May 2, 2026

卡顿-离屏渲染

离屏渲染(Offscreen Rendering)是GPU渲染中的性能杀手。本文介绍离屏渲染的原理、触发条件以及优化方案。 什么是离屏渲染 正常渲染流程 正常情况下,GPU直接将渲染结果写入帧缓冲区(Frame Buffer): flowchart LR subgraph 正常渲染["正常渲染 On-Screen Rendering"] A[GPU渲染] --> B[Frame Buffer帧缓冲区] --> C[屏幕显示] end style A fill:#e1f5fe style B fill:#fff3e0 style C fill:#e8f5e9 特点: 直接渲染到帧缓冲区 渲染完成即可显示 效率最高 离屏渲染流程 离屏渲染需要先渲染到离屏缓冲区,再合成到帧缓冲区: flowchart LR subgraph 离屏渲染["离屏渲染 Offscreen Rendering"] A[GPU渲染] --> B[Offscreen Buffer离屏缓冲区] B --> C[Frame Buffer帧缓冲区] C --> D[屏幕显示] end E[额外的缓冲区] -.-> B style A fill:#e1f5fe style B fill:#f48fb1 style C fill:#fff3e0 style D fill:#e8f5e9 style E fill:#f48fb1,stroke:#f44336,stroke-dasharray: 5 5 性能开销: ...

May 2, 2026

卡顿-图片优化

图片处理是iOS应用中常见的性能瓶颈。本文介绍图片解码原理以及各种优化方案。 图片解码原理 为什么需要解码 图片文件(PNG、JPEG等)是压缩格式,无法直接显示。GPU需要的是位图(Bitmap)格式: flowchart LR A[压缩图片PNG/JPEG] --> B[解码DecodeCPU密集操作] B --> C[位图BitmapGPU可直接使用] 位图大小 = 宽度 × 高度 × 每像素字节数 例如:1000×1000 RGBA图片 = 1000 × 1000 × 4 = 4MB 默认解码时机 // 加载图片(此时未解码) let image = UIImage(named: "large_image") // 设置到ImageView(仍未解码) imageView.image = image // 在 CATransaction commit 的 prepare 阶段,Core Animation 会在主线程对未解码的图片执行解码 // 未提前解码的图片一定会在此阶段被解码,从而阻塞主线程引发卡顿 异步解码 基本原理 将解码工作移到后台线程: sequenceDiagram participant M as 主线程 participant B as 后台线程 M->>B: 请求加载图片 Note over M: 继续处理其他事件 B->>B: 加载压缩数据 B->>B: 解码为位图 B->>B: 创建CGImage B->>M: 返回解码后的图片 M->>M: 更新UI 实现方案1:强制解码 extension UIImage { /// 强制解码图片 func decodedImage() -> UIImage? { guard let cgImage = self.cgImage else { return nil } let width = cgImage.width let height = cgImage.height // 创建位图上下文 let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) guard let context = CGContext( data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue ) else { return nil } // 绘制到上下文(触发解码) context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) // 从上下文创建新图片 guard let decodedCGImage = context.makeImage() else { return nil } return UIImage(cgImage: decodedCGImage, scale: scale, orientation: imageOrientation) } } // 异步使用 func loadImageAsync(named name: String, completion: @escaping (UIImage?) -> Void) { DispatchQueue.global(qos: .userInitiated).async { let image = UIImage(named: name)?.decodedImage() DispatchQueue.main.async { completion(image) } } } 实现方案2:使用ImageIO import ImageIO class ImageDecoder { static func decodeImage(from url: URL) -> UIImage? { guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { return nil } let options: [CFString: Any] = [ kCGImageSourceShouldCache: true, kCGImageSourceShouldCacheImmediately: true // 立即解码 ] guard let cgImage = CGImageSourceCreateImageAtIndex(source, 0, options as CFDictionary) else { return nil } return UIImage(cgImage: cgImage) } static func decodeImageAsync(from url: URL, completion: @escaping (UIImage?) -> Void) { DispatchQueue.global(qos: .userInitiated).async { let image = ImageDecoder.decodeImage(from: url) DispatchQueue.main.async { completion(image) } } } } 实现方案3:使用UIGraphicsImageRenderer extension UIImage { func decodedImageUsingRenderer() -> UIImage { let format = UIGraphicsImageRendererFormat() format.scale = scale format.opaque = true format.preferredRange = .standard let renderer = UIGraphicsImageRenderer(size: size, format: format) return renderer.image { context in draw(at: .zero) } } } 图片降采样 为什么需要降采样 当图片尺寸远大于显示尺寸时,加载原图是浪费: ...

May 2, 2026